@mlx-node/lm 0.0.10 → 0.0.13
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/chat-session.d.ts +175 -68
- package/dist/chat-session.d.ts.map +1 -1
- package/dist/chat-session.js +271 -122
- package/dist/family-data.d.ts +407 -0
- package/dist/family-data.d.ts.map +1 -0
- package/dist/family-data.js +381 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/models/model-loader.d.ts +91 -135
- package/dist/models/model-loader.d.ts.map +1 -1
- package/dist/models/model-loader.js +74 -179
- package/dist/models/paged-config-override.d.ts +0 -2
- package/dist/models/paged-config-override.d.ts.map +1 -1
- package/dist/models/paged-config-override.js +29 -6
- package/dist/models/qwen3_5-configs.d.ts +1 -3
- package/dist/models/qwen3_5-configs.d.ts.map +1 -1
- package/dist/stream.d.ts +29 -5
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +127 -24
- package/package.json +7 -3
- package/dist/interfaces.d.ts +0 -3
- package/dist/interfaces.d.ts.map +0 -1
- package/dist/interfaces.js +0 -1
|
@@ -1,153 +1,105 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
12
|
-
* model.safetensors) loaded alongside the target
|
|
13
|
-
*
|
|
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`.
|
|
22
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
55
|
-
readonly
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
readonly
|
|
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
|
|
62
|
-
readonly
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
82
|
-
readonly
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
94
|
-
readonly
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
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
|
|
104
|
-
readonly
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
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
|
|
114
|
-
readonly
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
125
|
-
readonly
|
|
126
|
-
readonly
|
|
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
|
|
136
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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<
|
|
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
|
|
114
|
+
* FFI instances. Both sides derive from the same trainable family ids.
|
|
163
115
|
*/
|
|
164
|
-
export type TrainableModel = Awaited<ReturnType<
|
|
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
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
|
195
|
-
*
|
|
196
|
-
*
|
|
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
|
|
200
|
-
* reports `hasMtpWeights()`
|
|
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
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
190
|
-
* `draftModelPath` reaches
|
|
191
|
-
* it loudly instead of silently ignoring
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
|
209
|
-
*
|
|
210
|
-
*
|
|
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
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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
|
|
240
|
-
* reports `hasMtpWeights()`
|
|
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 =
|
|
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
|
-
|
|
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;
|
|
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"}
|