@mlx-node/lm 0.0.9 → 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.
@@ -1,153 +1,105 @@
1
1
  /**
2
- * Model loading utilities for Qwen3 models
3
- *
4
- * Handles loading pretrained weights from MLX format or converting from HuggingFace.
2
+ * Native half of the family registry: one loader binding per
3
+ * `MODEL_FAMILY_DATA` row, plus `detectModelType` (filesystem + GGUF).
5
4
  */
6
- import { Gemma4Model as NativeGemma4Model, HarrierModel, Lfm2Model as NativeLfm2Model, QianfanOCRModel, Qwen3Model as NativeQwen3Model, Qwen35Model as NativeQwen35Model, Qwen35MoeModel as NativeQwen35MoeModel } from '@mlx-node/core';
5
+ import { Gemma4Model as NativeGemma4Model, HarrierModel, Lfm2Model as NativeLfm2Model, MuseGlimmerModel as NativeMuseGlimmerModel, NemotronHModel as NativeNemotronHModel, QianfanOCRModel, Qwen3Model as NativeQwen3Model, Qwen35Model as NativeQwen35Model, Qwen35MoeModel as NativeQwen35MoeModel } from '@mlx-node/core';
7
6
  import { ChatSession, type SessionCapableModel } from '../chat-session.js';
7
+ import { type ModelType, type TrainableFamilyId } from '../family-data.js';
8
8
  /** Optional settings for {@link loadModel} / {@link loadSession}. */
9
9
  export interface LoadModelOptions {
10
10
  /**
11
- * Gemma4 only: directory of an external draft checkpoint (config.json +
12
- * model.safetensors) loaded alongside the target model for speculative
13
- * decoding (forwarded as `Gemma4LoadOptions.draftModelPath`). Accepts
14
- * either a DSpark draft or a Google gemma-4 assistant draft
11
+ * Directory of an external draft checkpoint (config.json +
12
+ * model.safetensors) loaded alongside the target for speculative decoding.
13
+ * Gemma4 accepts either a DSpark draft or a Google gemma-4 assistant draft
15
14
  * (`google/gemma-4-*-it-assistant`); the variant is auto-detected from
16
15
  * the draft's config.json (`model_type` `gemma4_assistant` /
17
16
  * `gemma4_unified_assistant` → assistant, `architectures` containing
18
17
  * `Gemma4DSparkModel` → DSpark). When omitted, Gemma4 automatically loads
19
18
  * an embedded draft from `<modelPath>/draft/` when present. Draft decoding
20
19
  * runs on the flat KV-cache path, so the target checkpoint must not
21
- * explicitly enable `use_block_paged_cache`. Setting this for any other
22
- * model family is a hard error — no other loader accepts a draft model.
20
+ * explicitly enable `use_block_paged_cache`.
21
+ *
22
+ * Dense `qwen3_5` accepts a z-lab `DFlash2DraftModel` companion such as
23
+ * `z-lab/Qwen3.8-27B-DFlash2`. It shares the Qwen3.8 target embedding and
24
+ * LM head, validates all companion tensors at load time, and takes
25
+ * precedence over an inline target MTP head. Other model families reject
26
+ * this option.
23
27
  */
24
28
  draftModelPath?: string;
25
29
  }
26
- interface NormalizedModelConfig {
27
- readonly usesDefaultModelType: boolean;
28
- readonly rawModelType: string | undefined;
29
- readonly rawModelTypeLabel: string;
30
- readonly architectures: ReadonlySet<string>;
31
- }
32
- interface ModelConfigMatchContext extends NormalizedModelConfig {
33
- readonly modelType: string | undefined;
34
- }
35
30
  /**
36
- * Ordered source of truth for every supported model family. Each entry owns
37
- * its canonical `ModelType`, raw config aliases / architecture probes, loader,
38
- * and `ChatSession` eligibility:
39
- *
40
- * - `'trainable'` — GRPO/SFT-capable LM (Qwen3 family); chat-capable.
41
- * - `'loadable'` — chat-capable LM with no trainer engine (Gemma4, LFM2).
42
- * - `'embedding'` — no chat surface (Harrier); rejected by `loadSession`.
43
- * - `'vlm'` — VLM whose AsyncGenerator wrapper lives in
44
- * `@mlx-node/vlm` (importing it here would create a
45
- * circular package dependency), so `loadSession`
46
- * rejects it and routes callers to `@mlx-node/vlm`.
47
- *
48
- * A base family is selected from an explicit alias or the single declarative
49
- * nullish-model_type default, then architecture probes refine it in declaration
50
- * order. Gemma's unified architecture is authoritative (matching the native
51
- * loader); Harrier refines a Qwen3 base. Adding a family means adding one
52
- * descriptor here, without a second normalization or dispatch branch.
31
+ * Native half of the family registry: one loader + native class per
32
+ * `MODEL_FAMILY_DATA` row (the native-free half in `../family-data.ts`).
33
+ * `satisfies Record<ModelType, LoaderBinding>` makes the zip exhaustive both
34
+ * ways — a data row without a binding, or a binding without a row, fails to
35
+ * compile.
53
36
  */
54
- declare const MODEL_FAMILY_REGISTRY: readonly [{
55
- readonly modelType: 'gemma4';
56
- readonly kind: 'loadable';
57
- readonly match: {
58
- readonly rawModelTypes: readonly ["gemma4", "gemma4_text", "gemma4_unified"];
59
- readonly architectureProbe: ({ architectures }: ModelConfigMatchContext) => boolean;
37
+ declare const LOADER_BINDINGS: {
38
+ readonly gemma4: {
39
+ readonly load: (modelPath: string, options?: LoadModelOptions) => Promise<import("../stream.js").StreamingInstance<typeof NativeGemma4Model, {
40
+ readonly recordModelPath: true;
41
+ }>>;
42
+ readonly nativeModelClass: typeof NativeGemma4Model;
43
+ };
44
+ readonly muse_glimmer: {
45
+ readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeMuseGlimmerModel, {
46
+ readonly recordModelPath: true;
47
+ }>>;
48
+ readonly nativeModelClass: typeof NativeMuseGlimmerModel;
49
+ };
50
+ readonly harrier: {
51
+ readonly load: (modelPath: string) => Promise<HarrierModel>;
52
+ readonly nativeModelClass: typeof HarrierModel;
60
53
  };
61
- readonly load: (modelPath: string, options?: LoadModelOptions) => Promise<import("../stream.js").StreamingInstance<typeof NativeGemma4Model, {
62
- readonly recordModelPath: true;
63
- }>>;
64
- readonly nativeModelClass: typeof NativeGemma4Model;
65
- readonly acceptsDraftModel: true;
66
- }, {
67
- readonly modelType: 'harrier';
68
- readonly kind: 'embedding';
69
- readonly match: {
70
- readonly rawModelTypes: readonly ["harrier"];
71
- readonly architectureProbe: ({ modelType, architectures }: ModelConfigMatchContext) => boolean;
54
+ readonly qwen3: {
55
+ readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeQwen3Model, {
56
+ readonly recordModelPath: true;
57
+ readonly applyTemplate: false;
58
+ }>>;
59
+ readonly nativeModelClass: typeof NativeQwen3Model;
72
60
  };
73
- readonly load: (modelPath: string) => Promise<HarrierModel>;
74
- readonly nativeModelClass: typeof HarrierModel;
75
- }, {
76
- readonly modelType: 'qwen3';
77
- readonly kind: 'trainable';
78
- readonly match: {
79
- readonly rawModelTypes: readonly ["qwen3"];
61
+ readonly qwen3_5: {
62
+ readonly load: (modelPath: string, options?: LoadModelOptions) => Promise<import("../stream.js").StreamingInstance<typeof NativeQwen35Model, {
63
+ readonly recordModelPath: true;
64
+ }>>;
65
+ readonly nativeModelClass: typeof NativeQwen35Model;
80
66
  };
81
- readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeQwen3Model, {
82
- readonly recordModelPath: true;
83
- readonly applyTemplate: false;
84
- }>>;
85
- readonly nativeModelClass: typeof NativeQwen3Model;
86
- readonly defaultForNullishModelType: true;
87
- }, {
88
- readonly modelType: 'qwen3_5';
89
- readonly kind: 'trainable';
90
- readonly match: {
91
- readonly rawModelTypes: readonly ["qwen3_5"];
67
+ readonly qwen3_5_moe: {
68
+ readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeQwen35MoeModel, {
69
+ readonly recordModelPath: true;
70
+ }>>;
71
+ readonly nativeModelClass: typeof NativeQwen35MoeModel;
92
72
  };
93
- readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeQwen35Model, {
94
- readonly recordModelPath: true;
95
- }>>;
96
- readonly nativeModelClass: typeof NativeQwen35Model;
97
- }, {
98
- readonly modelType: 'qwen3_5_moe';
99
- readonly kind: 'trainable';
100
- readonly match: {
101
- readonly rawModelTypes: readonly ["qwen3_5_moe"];
73
+ readonly lfm2: {
74
+ readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeLfm2Model, {
75
+ readonly recordModelPath: true;
76
+ readonly replayAssistantRawText: true;
77
+ }>>;
78
+ readonly nativeModelClass: typeof NativeLfm2Model;
102
79
  };
103
- readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeQwen35MoeModel, {
104
- readonly recordModelPath: true;
105
- }>>;
106
- readonly nativeModelClass: typeof NativeQwen35MoeModel;
107
- }, {
108
- readonly modelType: 'lfm2';
109
- readonly kind: 'loadable';
110
- readonly match: {
111
- readonly rawModelTypes: readonly ["lfm2"];
80
+ readonly lfm2_moe: {
81
+ readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeLfm2Model, {
82
+ readonly recordModelPath: true;
83
+ readonly replayAssistantRawText: true;
84
+ }>>;
85
+ readonly nativeModelClass: typeof NativeLfm2Model;
112
86
  };
113
- readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeLfm2Model, {
114
- readonly recordModelPath: true;
115
- readonly replayAssistantRawText: true;
116
- }>>;
117
- readonly nativeModelClass: typeof NativeLfm2Model;
118
- }, {
119
- readonly modelType: 'lfm2_moe';
120
- readonly kind: 'loadable';
121
- readonly match: {
122
- readonly rawModelTypes: readonly ["lfm2_moe"];
87
+ readonly nemotron_h: {
88
+ readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeNemotronHModel, {
89
+ readonly recordModelPath: true;
90
+ }>>;
91
+ readonly nativeModelClass: typeof NativeNemotronHModel;
123
92
  };
124
- readonly load: (modelPath: string) => Promise<import("../stream.js").StreamingInstance<typeof NativeLfm2Model, {
125
- readonly recordModelPath: true;
126
- readonly replayAssistantRawText: true;
127
- }>>;
128
- readonly nativeModelClass: typeof NativeLfm2Model;
129
- }, {
130
- readonly modelType: 'internvl_chat';
131
- readonly kind: 'vlm';
132
- readonly match: {
133
- readonly rawModelTypes: readonly ["internvl_chat"];
93
+ readonly internvl_chat: {
94
+ readonly load: (modelPath: string) => Promise<QianfanOCRModel>;
95
+ readonly nativeModelClass: typeof QianfanOCRModel;
134
96
  };
135
- readonly load: (modelPath: string) => Promise<QianfanOCRModel>;
136
- readonly nativeModelClass: typeof QianfanOCRModel;
137
- }, {
138
- readonly modelType: 'qianfan-ocr';
139
- readonly kind: 'vlm';
140
- readonly match: {
141
- readonly rawModelTypes: readonly ["qianfan-ocr"];
97
+ readonly 'qianfan-ocr': {
98
+ readonly load: (modelPath: string) => Promise<QianfanOCRModel>;
99
+ readonly nativeModelClass: typeof QianfanOCRModel;
142
100
  };
143
- readonly load: (modelPath: string) => Promise<QianfanOCRModel>;
144
- readonly nativeModelClass: typeof QianfanOCRModel;
145
- }];
146
- export type ModelType = (typeof MODEL_FAMILY_REGISTRY)[number]['modelType'];
147
- type RegisteredModelFamily = (typeof MODEL_FAMILY_REGISTRY)[number];
148
- type RegisteredTrainableFamily = Extract<RegisteredModelFamily, {
149
- readonly kind: 'trainable';
150
- }>;
101
+ };
102
+ type LoaderBindings = typeof LOADER_BINDINGS;
151
103
  /**
152
104
  * Union of the native `@mlx-node/core` model classes across every registered
153
105
  * family — the public contract of {@link loadModel}. At runtime the chat
@@ -156,21 +108,21 @@ type RegisteredTrainableFamily = Extract<RegisteredModelFamily, {
156
108
  * native classes so downstream code can pass instances directly to Rust
157
109
  * engine factory methods without type conflicts.
158
110
  */
159
- export type LoadableModel = InstanceType<RegisteredModelFamily['nativeModelClass']>;
111
+ export type LoadableModel = InstanceType<LoaderBindings[ModelType]['nativeModelClass']>;
160
112
  /**
161
113
  * Union accepted by trainer APIs: registered wrapper results plus their native
162
- * FFI instances. Both sides derive from the same trainable registry rows.
114
+ * FFI instances. Both sides derive from the same trainable family ids.
163
115
  */
164
- export type TrainableModel = Awaited<ReturnType<RegisteredTrainableFamily['load']>> | InstanceType<RegisteredTrainableFamily['nativeModelClass']>;
116
+ export type TrainableModel = Awaited<ReturnType<LoaderBindings[TrainableFamilyId]['load']>> | InstanceType<LoaderBindings[TrainableFamilyId]['nativeModelClass']>;
165
117
  /**
166
118
  * Load a model from disk, auto-detecting architecture from config.json.
167
119
  *
168
120
  * Supports both language models (Qwen3, Qwen3.5) and vision-language models
169
121
  * (Qianfan-OCR / InternVL). Use `instanceof` to narrow the returned type.
170
122
  *
171
- * `options.draftModelPath` attaches an external draft checkpoint (DSpark or
172
- * Google gemma-4 assistant, auto-detected from the draft's config.json) for
173
- * speculative decoding — gemma4 only; any other detected family rejects it.
123
+ * `options.draftModelPath` attaches an external draft checkpoint for
124
+ * speculative decoding gemma4 and dense qwen3_5 only; every other family
125
+ * rejects it.
174
126
  * Without the option, Gemma4 loads `<modelPath>/draft/` automatically when
175
127
  * that embedded checkpoint is present.
176
128
  */
@@ -191,13 +143,17 @@ export declare function loadModel(modelPath: string, options?: LoadModelOptions)
191
143
  * must import `QianfanOCRModel` from `@mlx-node/vlm` and construct
192
144
  * `new ChatSession(model)` directly.
193
145
  *
194
- * `options.draftModelPath` attaches an external draft checkpoint (DSpark or
195
- * Google gemma-4 assistant, auto-detected from the draft's config.json) for
196
- * speculative decoding — gemma4 only; any other detected family rejects it.
146
+ * `options.draftModelPath` attaches an external draft checkpoint for
147
+ * speculative decoding gemma4 and dense qwen3_5 only; every other family
148
+ * rejects it.
197
149
  * Without the option, Gemma4 loads `<modelPath>/draft/` automatically when
198
150
  * that embedded checkpoint is present.
199
- * The resulting session auto-enables the speculative path (the model
200
- * reports `hasMtpWeights()`); pass `enableMtp: false` per call to opt out.
151
+ * The resulting session auto-enables the speculative path when the model
152
+ * reports `hasMtpWeights()` AND does not opt out of the auto-default; pass
153
+ * `enableMtp: false` per call to suppress it, or `enableMtp: true` to force
154
+ * it on a family that opts out. NemotronH opts out (`mtpAutoEnabled()`
155
+ * returns false) because forcing MTP moves the turn into the exclusive lane
156
+ * and out of continuous batching; see `ChatSession.mtpAutoDefaultAllowed`.
201
157
  */
202
158
  export declare function loadSession(modelPath: string, options?: LoadModelOptions): Promise<ChatSession<SessionCapableModel>>;
203
159
  export declare function detectModelType(modelPath: string): Promise<ModelType>;
@@ -1 +1 @@
1
- {"version":3,"file":"model-loader.d.ts","sourceRoot":"","sources":["../../src/models/model-loader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EACL,WAAW,IAAI,iBAAiB,EAChC,YAAY,EACZ,SAAS,IAAI,eAAe,EAC5B,eAAe,EACf,UAAU,IAAI,gBAAgB,EAC9B,WAAW,IAAI,iBAAiB,EAChC,cAAc,IAAI,oBAAoB,EACvC,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAG3E,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID,UAAU,qBAAqB;IAC7B,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC7C;AAED,UAAU,uBAAwB,SAAQ,qBAAqB;IAC7D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAgCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,QAAA,MAAM,qBAAqB;wBAEZ,QAAQ;mBACb,UAAU;;;;;+BAKE,MAAM,YAAY,gBAAgB;;;;;;wBASzC,SAAS;mBACd,WAAW;;;;;+BAMC,MAAM;;;wBAIb,OAAO;mBACZ,WAAW;;iBACR,aAAa;;+BACJ,MAAM;;;;;;;wBAKb,SAAS;mBACd,WAAW;;iBACR,aAAa;;+BACJ,MAAM;;;;;wBAIb,aAAa;mBAClB,WAAW;;iBACR,aAAa;;+BACJ,MAAM;;;;;wBAIb,MAAM;mBACX,UAAU;;iBACP,aAAa;;+BACJ,MAAM;;;;;;wBAIb,UAAU;mBACf,UAAU;;iBACP,aAAa;;+BACJ,MAAM;;;;;;wBAIb,eAAe;mBACpB,KAAK;;iBACF,aAAa;;+BACJ,MAAM;;;wBAIb,aAAa;mBAClB,KAAK;;iBACF,aAAa;;+BACJ,MAAM;;EAGyB,CAAC;AAEtD,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,CAAC;AAE5E,KAAK,qBAAqB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AACpE,KAAK,yBAAyB,GAAG,OAAO,CAAC,qBAAqB,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC;AAEhG;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC,qBAAqB,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAEpF;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB,OAAO,CAAC,UAAU,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC,CAAC,GACtD,YAAY,CAAC,yBAAyB,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAsIhE;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAGrG;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,WAAW,CAAC,mBAAmB,CAAC,CAAC,CAa3C;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAkB3E"}
1
+ {"version":3,"file":"model-loader.d.ts","sourceRoot":"","sources":["../../src/models/model-loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EACL,WAAW,IAAI,iBAAiB,EAEhC,YAAY,EACZ,SAAS,IAAI,eAAe,EAC5B,gBAAgB,IAAI,sBAAsB,EAC1C,cAAc,IAAI,oBAAoB,EACtC,eAAe,EACf,UAAU,IAAI,gBAAgB,EAC9B,WAAW,IAAI,iBAAiB,EAChC,cAAc,IAAI,oBAAoB,EACvC,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAML,KAAK,SAAS,EACd,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAW3B,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAmBD;;;;;;GAMG;AACH,QAAA,MAAM,eAAe;;iBAEjB,IAAI,cAAc,MAAM,YAAY,gBAAgB;;;iBAKpD,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;;;iBACxB,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;iBACxB,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;;;;iBACxB,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM,YAAY,gBAAgB;;;iBAKpD,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;;;iBACxB,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;;;;iBACxB,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;;;;iBACxB,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;;;iBACxB,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;iBACxB,gBAAgB;;;iBAGhB,IAAI,cAAc,MAAM;iBACxB,gBAAgB;;CAEiC,CAAC;AAEtD,KAAK,cAAc,GAAG,OAAO,eAAe,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAExF;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAC9D,YAAY,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAsCxE;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAGrG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,WAAW,CAAC,mBAAmB,CAAC,CAAC,CAa3C;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAwB3E"}
@@ -1,203 +1,88 @@
1
1
  /**
2
- * Model loading utilities for Qwen3 models
3
- *
4
- * Handles loading pretrained weights from MLX format or converting from HuggingFace.
2
+ * Native half of the family registry: one loader binding per
3
+ * `MODEL_FAMILY_DATA` row, plus `detectModelType` (filesystem + GGUF).
5
4
  */
6
5
  import { readFile } from 'node:fs/promises';
7
- import { join } from 'node:path';
8
- import { Gemma4Model as NativeGemma4Model, HarrierModel, Lfm2Model as NativeLfm2Model, QianfanOCRModel, Qwen3Model as NativeQwen3Model, Qwen35Model as NativeQwen35Model, Qwen35MoeModel as NativeQwen35MoeModel, } from '@mlx-node/core';
6
+ import { dirname, extname, join } from 'node:path';
7
+ import { Gemma4Model as NativeGemma4Model, ggufArchitecture, HarrierModel, Lfm2Model as NativeLfm2Model, MuseGlimmerModel as NativeMuseGlimmerModel, NemotronHModel as NativeNemotronHModel, QianfanOCRModel, Qwen3Model as NativeQwen3Model, Qwen35Model as NativeQwen35Model, Qwen35MoeModel as NativeQwen35MoeModel, } from '@mlx-node/core';
9
8
  import { ChatSession } from '../chat-session.js';
10
- import { Gemma4Model, Lfm2Model, Qwen3Model, Qwen35Model, Qwen35MoeModel } from '../stream.js';
9
+ import { familyDataFor, MalformedModelConfigError, matchFamily, MODEL_FAMILY_DATA, UnsupportedModelTypeError, } from '../family-data.js';
10
+ import { Gemma4Model, Lfm2Model, MuseGlimmerModel, NemotronHModel, Qwen3Model, Qwen35Model, Qwen35MoeModel, } from '../stream.js';
11
11
  /**
12
- * Ordered source of truth for every supported model family. Each entry owns
13
- * its canonical `ModelType`, raw config aliases / architecture probes, loader,
14
- * and `ChatSession` eligibility:
15
- *
16
- * - `'trainable'` — GRPO/SFT-capable LM (Qwen3 family); chat-capable.
17
- * - `'loadable'` — chat-capable LM with no trainer engine (Gemma4, LFM2).
18
- * - `'embedding'` — no chat surface (Harrier); rejected by `loadSession`.
19
- * - `'vlm'` — VLM whose AsyncGenerator wrapper lives in
20
- * `@mlx-node/vlm` (importing it here would create a
21
- * circular package dependency), so `loadSession`
22
- * rejects it and routes callers to `@mlx-node/vlm`.
23
- *
24
- * A base family is selected from an explicit alias or the single declarative
25
- * nullish-model_type default, then architecture probes refine it in declaration
26
- * order. Gemma's unified architecture is authoritative (matching the native
27
- * loader); Harrier refines a Qwen3 base. Adding a family means adding one
28
- * descriptor here, without a second normalization or dispatch branch.
12
+ * Native half of the family registry: one loader + native class per
13
+ * `MODEL_FAMILY_DATA` row (the native-free half in `../family-data.ts`).
14
+ * `satisfies Record<ModelType, LoaderBinding>` makes the zip exhaustive both
15
+ * ways — a data row without a binding, or a binding without a row, fails to
16
+ * compile.
29
17
  */
30
- const MODEL_FAMILY_REGISTRY = [
31
- {
32
- modelType: 'gemma4',
33
- kind: 'loadable',
34
- match: {
35
- rawModelTypes: ['gemma4', 'gemma4_text', 'gemma4_unified'],
36
- architectureProbe: ({ architectures }) => architectures.has('Gemma4UnifiedForConditionalGeneration'),
37
- },
18
+ const LOADER_BINDINGS = {
19
+ gemma4: {
38
20
  load: (modelPath, options) => Gemma4Model.load(modelPath, options?.draftModelPath === undefined ? null : { draftModelPath: options.draftModelPath }),
39
21
  nativeModelClass: NativeGemma4Model,
40
- acceptsDraftModel: true,
41
22
  },
42
- {
43
- modelType: 'harrier',
44
- kind: 'embedding',
45
- match: {
46
- rawModelTypes: ['harrier'],
47
- architectureProbe: ({ modelType, architectures }) => modelType === 'qwen3' && architectures.has('Qwen3Model') && !architectures.has('Qwen3ForCausalLM'),
48
- },
23
+ muse_glimmer: {
24
+ load: (modelPath) => MuseGlimmerModel.load(modelPath),
25
+ nativeModelClass: NativeMuseGlimmerModel,
26
+ },
27
+ harrier: {
49
28
  load: (modelPath) => HarrierModel.load(modelPath),
50
29
  nativeModelClass: HarrierModel,
51
30
  },
52
- {
53
- modelType: 'qwen3',
54
- kind: 'trainable',
55
- match: { rawModelTypes: ['qwen3'] },
31
+ qwen3: {
56
32
  load: (modelPath) => Qwen3Model.load(modelPath),
57
33
  nativeModelClass: NativeQwen3Model,
58
- defaultForNullishModelType: true,
59
34
  },
60
- {
61
- modelType: 'qwen3_5',
62
- kind: 'trainable',
63
- match: { rawModelTypes: ['qwen3_5'] },
64
- load: (modelPath) => Qwen35Model.load(modelPath),
35
+ qwen3_5: {
36
+ load: (modelPath, options) => Qwen35Model.load(modelPath, options?.draftModelPath === undefined ? null : { draftModelPath: options.draftModelPath }),
65
37
  nativeModelClass: NativeQwen35Model,
66
38
  },
67
- {
68
- modelType: 'qwen3_5_moe',
69
- kind: 'trainable',
70
- match: { rawModelTypes: ['qwen3_5_moe'] },
39
+ qwen3_5_moe: {
71
40
  load: (modelPath) => Qwen35MoeModel.load(modelPath),
72
41
  nativeModelClass: NativeQwen35MoeModel,
73
42
  },
74
- {
75
- modelType: 'lfm2',
76
- kind: 'loadable',
77
- match: { rawModelTypes: ['lfm2'] },
43
+ lfm2: {
78
44
  load: (modelPath) => Lfm2Model.load(modelPath),
79
45
  nativeModelClass: NativeLfm2Model,
80
46
  },
81
- {
82
- modelType: 'lfm2_moe',
83
- kind: 'loadable',
84
- match: { rawModelTypes: ['lfm2_moe'] },
47
+ lfm2_moe: {
85
48
  load: (modelPath) => Lfm2Model.load(modelPath),
86
49
  nativeModelClass: NativeLfm2Model,
87
50
  },
88
- {
89
- modelType: 'internvl_chat',
90
- kind: 'vlm',
91
- match: { rawModelTypes: ['internvl_chat'] },
51
+ nemotron_h: {
52
+ load: (modelPath) => NemotronHModel.load(modelPath),
53
+ nativeModelClass: NativeNemotronHModel,
54
+ },
55
+ internvl_chat: {
92
56
  load: (modelPath) => QianfanOCRModel.load(modelPath),
93
57
  nativeModelClass: QianfanOCRModel,
94
58
  },
95
- {
96
- modelType: 'qianfan-ocr',
97
- kind: 'vlm',
98
- match: { rawModelTypes: ['qianfan-ocr'] },
59
+ 'qianfan-ocr': {
99
60
  load: (modelPath) => QianfanOCRModel.load(modelPath),
100
61
  nativeModelClass: QianfanOCRModel,
101
62
  },
102
- ];
103
- function buildModelFamilyIndex(registry) {
104
- const byModelType = new Map();
105
- const byRawModelType = new Map();
106
- let defaultForNullishModelType;
107
- for (const family of registry) {
108
- const previousFamily = byModelType.get(family.modelType);
109
- if (previousFamily !== undefined) {
110
- throw new Error(`Duplicate canonical model type "${family.modelType}" in model family registry`);
111
- }
112
- byModelType.set(family.modelType, family);
113
- for (const rawModelType of family.match.rawModelTypes) {
114
- const previous = byRawModelType.get(rawModelType);
115
- if (previous !== undefined) {
116
- throw new Error(`Duplicate model_type alias "${rawModelType}" for "${previous.modelType}" and "${family.modelType}"`);
117
- }
118
- byRawModelType.set(rawModelType, family);
119
- }
120
- if (family.defaultForNullishModelType === true) {
121
- if (defaultForNullishModelType !== undefined) {
122
- throw new Error(`Duplicate nullish-model_type defaults for "${defaultForNullishModelType.modelType}" and "${family.modelType}"`);
123
- }
124
- defaultForNullishModelType = family;
125
- }
126
- }
127
- if (defaultForNullishModelType === undefined) {
128
- throw new Error('Model family registry must declare exactly one nullish-model_type default');
129
- }
130
- return { byModelType, byRawModelType, defaultForNullishModelType };
131
- }
132
- const MODEL_FAMILY_INDEX = buildModelFamilyIndex(MODEL_FAMILY_REGISTRY);
133
- function findFamily(modelType) {
134
- const family = MODEL_FAMILY_INDEX.byModelType.get(modelType);
63
+ };
64
+ // Only families whose native `load(path)` accepts a GGUF file carry a
65
+ // `ggufArchitectures` row entry (see family-data.ts).
66
+ const GGUF_ARCHITECTURE_MODEL_TYPES = new Map(MODEL_FAMILY_DATA.flatMap((row) => 'ggufArchitectures' in row ? row.ggufArchitectures.map((architecture) => [architecture, row.id]) : []));
67
+ function requireFamilyData(modelType) {
68
+ const family = familyDataFor(modelType);
135
69
  if (family === undefined) {
136
70
  throw new Error(`Internal error: missing model family descriptor for "${modelType}"`);
137
71
  }
138
72
  return family;
139
73
  }
140
- function matchesArchitectureProbe(family, config) {
141
- return family.match.architectureProbe?.(config) === true;
142
- }
143
- class MalformedModelConfigError extends Error {
144
- constructor(modelPath, reason) {
145
- super(`Malformed config.json in ${modelPath}: ${reason}`);
146
- this.name = 'MalformedModelConfigError';
147
- }
148
- }
149
- /**
150
- * Fail-closed validation: a config.json whose root is not a plain object,
151
- * or whose `architectures` is neither an array nor a string, is rejected
152
- * instead of coerced (coercion would fall through to the qwen3
153
- * nullish-model_type default and silently misroute the checkpoint).
154
- * Blessed lenient shapes stay accepted: `{}` root (qwen3 default),
155
- * missing/`null` `architectures` (empty set), bare-string `architectures`
156
- * (single-element set), and non-string array entries (filtered out).
157
- */
158
- function normalizeConfig(modelPath, config) {
159
- if (typeof config !== 'object' || config === null || Array.isArray(config)) {
160
- throw new MalformedModelConfigError(modelPath, 'root must be a JSON object');
161
- }
162
- const object = config;
163
- const hasModelType = Object.hasOwn(object, 'model_type');
164
- const rawModelTypeValue = hasModelType ? object.model_type : undefined;
165
- const usesDefaultModelType = !hasModelType || rawModelTypeValue === null;
166
- const rawModelType = typeof rawModelTypeValue === 'string' ? rawModelTypeValue : undefined;
167
- const rawModelTypeLabel = hasModelType ? String(rawModelTypeValue) : '<missing>';
168
- const rawArchitectures = 'architectures' in object ? object.architectures : undefined;
169
- if (rawArchitectures !== undefined &&
170
- rawArchitectures !== null &&
171
- !Array.isArray(rawArchitectures) &&
172
- typeof rawArchitectures !== 'string') {
173
- throw new MalformedModelConfigError(modelPath, '"architectures" must be an array or a string');
174
- }
175
- const architectures = Array.isArray(rawArchitectures)
176
- ? rawArchitectures.filter((architecture) => typeof architecture === 'string')
177
- : typeof rawArchitectures === 'string'
178
- ? [rawArchitectures]
179
- : [];
180
- return { usesDefaultModelType, rawModelType, rawModelTypeLabel, architectures: new Set(architectures) };
181
- }
182
- class UnsupportedModelTypeError extends Error {
183
- constructor(modelPath, rawModelTypeLabel) {
184
- super(`Unsupported model_type "${rawModelTypeLabel}" in ${modelPath}/config.json`);
185
- this.name = 'UnsupportedModelTypeError';
186
- }
187
- }
188
74
  /**
189
- * Dispatch a load through the registry, validating gemma4-only options.
190
- * `draftModelPath` reaches ONLY the gemma4 row; every other family rejects
191
- * it loudly instead of silently ignoring a caller's speculative-decode
192
- * intent.
75
+ * Dispatch a load through the registry, validating draft-capable families.
76
+ * `draftModelPath` reaches only gemma4 and dense qwen3_5; every other family
77
+ * rejects it loudly instead of silently ignoring speculative-decode intent.
193
78
  */
194
79
  function dispatchLoad(modelType, modelPath, options) {
195
- const family = findFamily(modelType);
196
- if (options?.draftModelPath !== undefined && family.acceptsDraftModel !== true) {
197
- throw new Error(`draftModelPath (speculative-decoding draft) is only supported by gemma4 models; ` +
80
+ if (options?.draftModelPath !== undefined && requireFamilyData(modelType).acceptsDraftModel !== true) {
81
+ throw new Error(`draftModelPath (speculative-decoding draft) is only supported by gemma4 and qwen3_5 models; ` +
198
82
  `${modelPath} has model_type "${modelType}"`);
199
83
  }
200
- return family.load(modelPath, options);
84
+ const binding = LOADER_BINDINGS[modelType];
85
+ return binding.load(modelPath, options);
201
86
  }
202
87
  /**
203
88
  * Load a model from disk, auto-detecting architecture from config.json.
@@ -205,9 +90,9 @@ function dispatchLoad(modelType, modelPath, options) {
205
90
  * Supports both language models (Qwen3, Qwen3.5) and vision-language models
206
91
  * (Qianfan-OCR / InternVL). Use `instanceof` to narrow the returned type.
207
92
  *
208
- * `options.draftModelPath` attaches an external draft checkpoint (DSpark or
209
- * Google gemma-4 assistant, auto-detected from the draft's config.json) for
210
- * speculative decoding — gemma4 only; any other detected family rejects it.
93
+ * `options.draftModelPath` attaches an external draft checkpoint for
94
+ * speculative decoding gemma4 and dense qwen3_5 only; every other family
95
+ * rejects it.
211
96
  * Without the option, Gemma4 loads `<modelPath>/draft/` automatically when
212
97
  * that embedded checkpoint is present.
213
98
  */
@@ -231,17 +116,21 @@ export async function loadModel(modelPath, options) {
231
116
  * must import `QianfanOCRModel` from `@mlx-node/vlm` and construct
232
117
  * `new ChatSession(model)` directly.
233
118
  *
234
- * `options.draftModelPath` attaches an external draft checkpoint (DSpark or
235
- * Google gemma-4 assistant, auto-detected from the draft's config.json) for
236
- * speculative decoding — gemma4 only; any other detected family rejects it.
119
+ * `options.draftModelPath` attaches an external draft checkpoint for
120
+ * speculative decoding gemma4 and dense qwen3_5 only; every other family
121
+ * rejects it.
237
122
  * Without the option, Gemma4 loads `<modelPath>/draft/` automatically when
238
123
  * that embedded checkpoint is present.
239
- * The resulting session auto-enables the speculative path (the model
240
- * reports `hasMtpWeights()`); pass `enableMtp: false` per call to opt out.
124
+ * The resulting session auto-enables the speculative path when the model
125
+ * reports `hasMtpWeights()` AND does not opt out of the auto-default; pass
126
+ * `enableMtp: false` per call to suppress it, or `enableMtp: true` to force
127
+ * it on a family that opts out. NemotronH opts out (`mtpAutoEnabled()`
128
+ * returns false) because forcing MTP moves the turn into the exclusive lane
129
+ * and out of continuous batching; see `ChatSession.mtpAutoDefaultAllowed`.
241
130
  */
242
131
  export async function loadSession(modelPath, options) {
243
132
  const modelType = await detectModelType(modelPath);
244
- const kind = findFamily(modelType).kind;
133
+ const kind = requireFamilyData(modelType).kind;
245
134
  if (kind === 'embedding') {
246
135
  throw new Error('loadSession: embedding models (Harrier) cannot be wrapped in a ChatSession');
247
136
  }
@@ -252,19 +141,25 @@ export async function loadSession(modelPath, options) {
252
141
  return new ChatSession(m);
253
142
  }
254
143
  export async function detectModelType(modelPath) {
144
+ const isGguf = extname(modelPath).toLowerCase() === '.gguf';
145
+ const configPath = isGguf ? join(dirname(modelPath), 'config.json') : join(modelPath, 'config.json');
146
+ let raw;
147
+ try {
148
+ raw = await readFile(configPath, 'utf-8');
149
+ }
150
+ catch (e) {
151
+ if (isGguf && typeof e === 'object' && e !== null && 'code' in e && e.code === 'ENOENT') {
152
+ const architecture = ggufArchitecture(modelPath);
153
+ const modelType = GGUF_ARCHITECTURE_MODEL_TYPES.get(architecture);
154
+ if (modelType === undefined) {
155
+ throw new Error(`Unsupported GGUF architecture "${architecture}" in ${modelPath}`);
156
+ }
157
+ return modelType;
158
+ }
159
+ throw new Error(`Cannot detect model type: config.json not found in ${modelPath}`);
160
+ }
255
161
  try {
256
- const raw = await readFile(join(modelPath, 'config.json'), 'utf-8');
257
- const config = normalizeConfig(modelPath, JSON.parse(raw));
258
- const baseFamily = config.usesDefaultModelType
259
- ? MODEL_FAMILY_INDEX.defaultForNullishModelType
260
- : config.rawModelType === undefined
261
- ? undefined
262
- : MODEL_FAMILY_INDEX.byRawModelType.get(config.rawModelType);
263
- const matchContext = { ...config, modelType: baseFamily?.modelType };
264
- const family = MODEL_FAMILY_REGISTRY.find((candidate) => matchesArchitectureProbe(candidate, matchContext)) ?? baseFamily;
265
- if (family === undefined)
266
- throw new UnsupportedModelTypeError(modelPath, config.rawModelTypeLabel);
267
- return family.modelType;
162
+ return matchFamily(modelPath, JSON.parse(raw));
268
163
  }
269
164
  catch (e) {
270
165
  if (e instanceof UnsupportedModelTypeError || e instanceof MalformedModelConfigError)
@@ -9,8 +9,6 @@
9
9
  * Gemma4 `draft/`) remain hidden unless a caller explicitly preserves the
10
10
  * Gemma draft for flat speculative decoding.
11
11
  */
12
- /** Every chat-capable family currently discovered by `@mlx-node/agent`. */
13
- export declare const AGENT_PAGED_MODEL_TYPES: readonly ['qwen3', 'qwen3_5', 'qwen3_5_moe', 'gemma4', 'lfm2', 'lfm2_moe'];
14
12
  /** Families historically forced paged by `mlx launch claude`. */
15
13
  export declare const QWEN35_PAGED_MODEL_TYPES: readonly ['qwen3_5', 'qwen3_5_moe'];
16
14
  export interface PagedConfigOverrideManagerOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"paged-config-override.d.ts","sourceRoot":"","sources":["../../src/models/paged-config-override.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,2EAA2E;AAC3E,eAAO,MAAM,uBAAuB,YAAI,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAU,CAAC;AAElH,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,YAAI,SAAS,EAAE,aAAa,CAAU,CAAC;AAO5E,MAAM,WAAW,iCAAiC;IAChD,qFAAqF;IACrF,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,qBAAa,0BAA0B;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAU;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8B;IAC7D,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,OAAO,GAAE,iCAAsC,EAI1D;IAED;;;;;;;;;;;;;;;;;OAiBG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAY1G;YAEa,eAAe;IAuE7B,6EAA6E;IAC7E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAKvB;YAEa,cAAc;YAad,cAAc;IAsD5B,OAAO,CAAC,OAAO;CAIhB"}
1
+ {"version":3,"file":"paged-config-override.d.ts","sourceRoot":"","sources":["../../src/models/paged-config-override.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,YAAI,SAAS,EAAE,aAAa,CAAU,CAAC;AAQ5E,MAAM,WAAW,iCAAiC;IAChD,qFAAqF;IACrF,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,qBAAa,0BAA0B;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAU;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8B;IAC7D,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,OAAO,GAAE,iCAAsC,EAI1D;IAED;;;;;;;;;;;;;;;;;OAiBG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAY1G;YAEa,eAAe;IAgF7B,6EAA6E;IAC7E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAKvB;YAEa,cAAc;YAad,cAAc;IA+D5B,OAAO,CAAC,OAAO;CAIhB"}