@mlx-node/lm 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/chat-session.d.ts +1 -1
  2. package/dist/chat-session.d.ts.map +1 -1
  3. package/dist/chat-session.js +2 -2
  4. package/dist/draft-companion.d.ts +16 -0
  5. package/dist/draft-companion.d.ts.map +1 -0
  6. package/dist/draft-companion.js +76 -0
  7. package/dist/family-data.d.ts +2 -0
  8. package/dist/family-data.d.ts.map +1 -1
  9. package/dist/family-data.js +2 -0
  10. package/dist/gguf-metadata.d.ts +2 -0
  11. package/dist/gguf-metadata.d.ts.map +1 -0
  12. package/dist/gguf-metadata.js +128 -0
  13. package/dist/model-detection.d.ts +6 -0
  14. package/dist/model-detection.d.ts.map +1 -0
  15. package/dist/model-detection.js +38 -0
  16. package/dist/model-discovery.d.ts +24 -0
  17. package/dist/model-discovery.d.ts.map +1 -0
  18. package/dist/model-discovery.js +274 -0
  19. package/dist/models/model-loader.d.ts +6 -0
  20. package/dist/models/model-loader.d.ts.map +1 -1
  21. package/dist/models/model-loader.js +10 -32
  22. package/dist/models/paged-config-override.d.ts.map +1 -1
  23. package/dist/models/paged-config-override.js +21 -1
  24. package/dist/stream.d.ts.map +1 -1
  25. package/dist/stream.js +5 -5
  26. package/package.json +21 -3
  27. package/src/chat-session.ts +2369 -0
  28. package/src/draft-companion.ts +74 -0
  29. package/src/family-data.ts +542 -0
  30. package/src/gguf-metadata.ts +117 -0
  31. package/src/index.ts +151 -0
  32. package/src/model-detection.ts +46 -0
  33. package/src/model-discovery.ts +329 -0
  34. package/src/models/lfm2-configs.ts +110 -0
  35. package/src/models/model-loader.ts +256 -0
  36. package/src/models/paged-config-override.ts +387 -0
  37. package/src/models/qwen3-configs.ts +113 -0
  38. package/src/models/qwen3_5-configs.ts +60 -0
  39. package/src/profiling.ts +69 -0
  40. package/src/stream.ts +960 -0
  41. package/src/tools/index.ts +58 -0
  42. package/src/tools/types.ts +215 -0
@@ -0,0 +1,110 @@
1
+ import type { Lfm2Config } from '@mlx-node/core';
2
+
3
+ export const LFM2_CONFIGS: { [key: string]: Lfm2Config } = {
4
+ 'lfm2.5-8b-a1b': {
5
+ vocabSize: 128000,
6
+ hiddenSize: 2048,
7
+ numHiddenLayers: 24,
8
+ numAttentionHeads: 32,
9
+ numKeyValueHeads: 8,
10
+ maxPositionEmbeddings: 128000,
11
+ normEps: 1e-5,
12
+ convBias: false,
13
+ convLCache: 3,
14
+ // block* fields are unused for MoE checkpoints (MoE dense layers use
15
+ // intermediateSize directly; MoE sparse layers use moeIntermediateSize).
16
+ // Required by the NAPI-derived Lfm2Config interface; set to 0 / defaults.
17
+ blockDim: 0,
18
+ blockFfDim: 0,
19
+ blockMultipleOf: 256,
20
+ blockFfnDimMultiplier: 1.0,
21
+ blockAutoAdjustFfDim: false,
22
+ ropeTheta: 5000000.0,
23
+ layerTypes: [
24
+ 'conv',
25
+ 'conv',
26
+ 'full_attention',
27
+ 'conv',
28
+ 'conv',
29
+ 'conv',
30
+ 'full_attention',
31
+ 'conv',
32
+ 'conv',
33
+ 'conv',
34
+ 'full_attention',
35
+ 'conv',
36
+ 'conv',
37
+ 'conv',
38
+ 'full_attention',
39
+ 'conv',
40
+ 'conv',
41
+ 'conv',
42
+ 'full_attention',
43
+ 'conv',
44
+ 'conv',
45
+ 'full_attention',
46
+ 'conv',
47
+ 'conv',
48
+ ],
49
+ tieEmbedding: true,
50
+ eosTokenId: 124900,
51
+ bosTokenId: 124894,
52
+ padTokenId: 124893,
53
+ intermediateSize: 7168,
54
+ moeIntermediateSize: 1792,
55
+ numExperts: 32,
56
+ numExpertsPerTok: 4,
57
+ numDenseLayers: 2,
58
+ normTopkProb: true,
59
+ useExpertBias: true,
60
+ },
61
+ 'lfm2.5-1.2b-thinking': {
62
+ vocabSize: 65536,
63
+ hiddenSize: 2048,
64
+ numHiddenLayers: 16,
65
+ numAttentionHeads: 32,
66
+ numKeyValueHeads: 8,
67
+ maxPositionEmbeddings: 128000,
68
+ normEps: 1e-5,
69
+ convBias: false,
70
+ convLCache: 3,
71
+ blockDim: 2048,
72
+ blockFfDim: 12288,
73
+ blockMultipleOf: 256,
74
+ blockFfnDimMultiplier: 1.0,
75
+ blockAutoAdjustFfDim: true,
76
+ ropeTheta: 1000000.0,
77
+ layerTypes: [
78
+ 'conv',
79
+ 'conv',
80
+ 'full_attention',
81
+ 'conv',
82
+ 'conv',
83
+ 'full_attention',
84
+ 'conv',
85
+ 'conv',
86
+ 'full_attention',
87
+ 'conv',
88
+ 'full_attention',
89
+ 'conv',
90
+ 'full_attention',
91
+ 'conv',
92
+ 'full_attention',
93
+ 'conv',
94
+ ],
95
+ tieEmbedding: true,
96
+ eosTokenId: 7,
97
+ bosTokenId: 1,
98
+ padTokenId: 0,
99
+ normTopkProb: true,
100
+ useExpertBias: true,
101
+ },
102
+ };
103
+
104
+ export function getLfm2Config(name: string): Lfm2Config {
105
+ const config = LFM2_CONFIGS[name];
106
+ if (!config) {
107
+ throw new Error(`Unknown LFM2 config: ${name}. Available: ${Object.keys(LFM2_CONFIGS).join(', ')}`);
108
+ }
109
+ return config;
110
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Native half of the family registry: one loader binding per
3
+ * `MODEL_FAMILY_DATA` row, plus `detectModelType` (filesystem + GGUF).
4
+ */
5
+
6
+ import {
7
+ Gemma4Model as NativeGemma4Model,
8
+ ggufArchitecture,
9
+ HarrierModel,
10
+ Lfm2Model as NativeLfm2Model,
11
+ MuseGlimmerModel as NativeMuseGlimmerModel,
12
+ NemotronHModel as NativeNemotronHModel,
13
+ QianfanOCRModel,
14
+ Qwen3Model as NativeQwen3Model,
15
+ Qwen35Model as NativeQwen35Model,
16
+ Qwen35MoeModel as NativeQwen35MoeModel,
17
+ } from '@mlx-node/core';
18
+
19
+ import { ChatSession, type SessionCapableModel } from '../chat-session.js';
20
+ import { findDFlash2Draft } from '../draft-companion.js';
21
+ import { familyDataFor, type ModelType, type TrainableFamilyId } from '../family-data.js';
22
+ import { detectModelType as detectLocalModelType } from '../model-detection.js';
23
+ import {
24
+ Gemma4Model,
25
+ Lfm2Model,
26
+ MuseGlimmerModel,
27
+ NemotronHModel,
28
+ Qwen3Model,
29
+ Qwen35Model,
30
+ Qwen35MoeModel,
31
+ } from '../stream.js';
32
+
33
+ /** Optional settings for {@link loadModel} / {@link loadSession}. */
34
+ export interface LoadModelOptions {
35
+ /** Discover a Qwen DFlash2 companion on disk (default true). Explicit draftModelPath wins. */
36
+ autoLoadDraft?: boolean;
37
+ /**
38
+ * Directory of an external draft checkpoint (config.json +
39
+ * model.safetensors) loaded alongside the target for speculative decoding.
40
+ * Gemma4 accepts either a DSpark draft or a Google gemma-4 assistant draft
41
+ * (`google/gemma-4-*-it-assistant`); the variant is auto-detected from
42
+ * the draft's config.json (`model_type` `gemma4_assistant` /
43
+ * `gemma4_unified_assistant` → assistant, `architectures` containing
44
+ * `Gemma4DSparkModel` → DSpark). When omitted, Gemma4 automatically loads
45
+ * an embedded draft from `<modelPath>/draft/` when present. Draft decoding
46
+ * runs on the flat KV-cache path, so the target checkpoint must not
47
+ * explicitly enable `use_block_paged_cache`.
48
+ *
49
+ * Dense `qwen3_5` accepts a z-lab `DFlash2DraftModel` companion such as
50
+ * `z-lab/Qwen3.8-27B-DFlash2`. It shares the Qwen3.8 target embedding and
51
+ * LM head, validates all companion tensors at load time, and takes
52
+ * precedence over an inline target MTP head. Other model families reject
53
+ * this option.
54
+ */
55
+ draftModelPath?: string;
56
+ }
57
+
58
+ type NativeModelClass = abstract new (...args: never[]) => object;
59
+
60
+ interface LoaderBinding {
61
+ readonly load: (modelPath: string, options?: LoadModelOptions) => Promise<unknown>;
62
+ /**
63
+ * Native `@mlx-node/core` class behind this family. The public
64
+ * `LoadableModel` / `TrainableModel` unions derive from these classes —
65
+ * NOT from the streaming-wrapper types the loaders return — so native
66
+ * instances stay assignable and trainers can pass them directly to the
67
+ * Rust engine factory methods without type conflicts. Loaded wrapper
68
+ * instances are runtime subclasses of their native class, so
69
+ * `instanceof` narrowing against these classes still works on
70
+ * `loadModel` results.
71
+ */
72
+ readonly nativeModelClass: NativeModelClass;
73
+ }
74
+
75
+ /**
76
+ * Native half of the family registry: one loader + native class per
77
+ * `MODEL_FAMILY_DATA` row (the native-free half in `../family-data.ts`).
78
+ * `satisfies Record<ModelType, LoaderBinding>` makes the zip exhaustive both
79
+ * ways — a data row without a binding, or a binding without a row, fails to
80
+ * compile.
81
+ */
82
+ const LOADER_BINDINGS = {
83
+ gemma4: {
84
+ load: (modelPath: string, options?: LoadModelOptions) =>
85
+ Gemma4Model.load(
86
+ modelPath,
87
+ options?.draftModelPath === undefined ? null : { draftModelPath: options.draftModelPath },
88
+ ),
89
+ nativeModelClass: NativeGemma4Model,
90
+ },
91
+ muse_glimmer: {
92
+ load: (modelPath: string) => MuseGlimmerModel.load(modelPath),
93
+ nativeModelClass: NativeMuseGlimmerModel,
94
+ },
95
+ harrier: {
96
+ load: (modelPath: string) => HarrierModel.load(modelPath),
97
+ nativeModelClass: HarrierModel,
98
+ },
99
+ qwen3: {
100
+ load: (modelPath: string) => Qwen3Model.load(modelPath),
101
+ nativeModelClass: NativeQwen3Model,
102
+ },
103
+ qwen3_5: {
104
+ load: (modelPath: string, options?: LoadModelOptions) =>
105
+ Qwen35Model.load(
106
+ modelPath,
107
+ options?.draftModelPath === undefined ? null : { draftModelPath: options.draftModelPath },
108
+ ),
109
+ nativeModelClass: NativeQwen35Model,
110
+ },
111
+ qwen3_5_moe: {
112
+ load: (modelPath: string) => Qwen35MoeModel.load(modelPath),
113
+ nativeModelClass: NativeQwen35MoeModel,
114
+ },
115
+ lfm2: {
116
+ load: (modelPath: string) => Lfm2Model.load(modelPath),
117
+ nativeModelClass: NativeLfm2Model,
118
+ },
119
+ lfm2_moe: {
120
+ load: (modelPath: string) => Lfm2Model.load(modelPath),
121
+ nativeModelClass: NativeLfm2Model,
122
+ },
123
+ nemotron_h: {
124
+ load: (modelPath: string) => NemotronHModel.load(modelPath),
125
+ nativeModelClass: NativeNemotronHModel,
126
+ },
127
+ internvl_chat: {
128
+ load: (modelPath: string) => QianfanOCRModel.load(modelPath),
129
+ nativeModelClass: QianfanOCRModel,
130
+ },
131
+ 'qianfan-ocr': {
132
+ load: (modelPath: string) => QianfanOCRModel.load(modelPath),
133
+ nativeModelClass: QianfanOCRModel,
134
+ },
135
+ } as const satisfies Record<ModelType, LoaderBinding>;
136
+
137
+ type LoaderBindings = typeof LOADER_BINDINGS;
138
+
139
+ /**
140
+ * Union of the native `@mlx-node/core` model classes across every registered
141
+ * family — the public contract of {@link loadModel}. At runtime the chat
142
+ * families resolve to streaming-wrapper subclasses of these classes
143
+ * (AsyncGenerator `chatStream*` overrides), but the public type names the
144
+ * native classes so downstream code can pass instances directly to Rust
145
+ * engine factory methods without type conflicts.
146
+ */
147
+ export type LoadableModel = InstanceType<LoaderBindings[ModelType]['nativeModelClass']>;
148
+
149
+ /**
150
+ * Union accepted by trainer APIs: registered wrapper results plus their native
151
+ * FFI instances. Both sides derive from the same trainable family ids.
152
+ */
153
+ export type TrainableModel =
154
+ | Awaited<ReturnType<LoaderBindings[TrainableFamilyId]['load']>>
155
+ | InstanceType<LoaderBindings[TrainableFamilyId]['nativeModelClass']>;
156
+
157
+ function requireFamilyData(modelType: ModelType) {
158
+ const family = familyDataFor(modelType);
159
+ if (family === undefined) {
160
+ throw new Error(`Internal error: missing model family descriptor for "${modelType}"`);
161
+ }
162
+ return family;
163
+ }
164
+
165
+ /**
166
+ * Dispatch a load through the registry, validating draft-capable families.
167
+ * `draftModelPath` reaches only gemma4 and dense qwen3_5; every other family
168
+ * rejects it loudly instead of silently ignoring speculative-decode intent.
169
+ */
170
+ function dispatchLoad(
171
+ modelType: ModelType,
172
+ modelPath: string,
173
+ options: LoadModelOptions | undefined,
174
+ ): Promise<unknown> {
175
+ if (options?.draftModelPath !== undefined && requireFamilyData(modelType).acceptsDraftModel !== true) {
176
+ throw new Error(
177
+ `draftModelPath (speculative-decoding draft) is only supported by gemma4 and qwen3_5 models; ` +
178
+ `${modelPath} has model_type "${modelType}"`,
179
+ );
180
+ }
181
+ const binding: LoaderBinding = LOADER_BINDINGS[modelType];
182
+ const draftModelPath =
183
+ options?.draftModelPath ?? (options?.autoLoadDraft === false ? undefined : findDFlash2Draft(modelPath, modelType));
184
+ return binding.load(modelPath, draftModelPath === undefined ? options : { ...options, draftModelPath });
185
+ }
186
+
187
+ /**
188
+ * Load a model from disk, auto-detecting architecture from config.json.
189
+ *
190
+ * Supports both language models (Qwen3, Qwen3.5) and vision-language models
191
+ * (Qianfan-OCR / InternVL). Use `instanceof` to narrow the returned type.
192
+ *
193
+ * `options.draftModelPath` attaches an external draft checkpoint for
194
+ * speculative decoding — gemma4 and dense qwen3_5 only; every other family
195
+ * rejects it.
196
+ * Without the option, Gemma4 loads `<modelPath>/draft/` automatically when
197
+ * that embedded checkpoint is present.
198
+ * Qwen also discovers `draft/` or a shared `qwen3.8-27b-dflash2` directory
199
+ * beside a Qwen3.8-27B target. Set `autoLoadDraft: false` to disable discovery.
200
+ */
201
+ export async function loadModel(modelPath: string, options?: LoadModelOptions): Promise<LoadableModel> {
202
+ const modelType = await detectModelType(modelPath);
203
+ return dispatchLoad(modelType, modelPath, options) as Promise<LoadableModel>;
204
+ }
205
+
206
+ /**
207
+ * Load a model and wrap it in a {@link ChatSession} for multi-turn chat.
208
+ *
209
+ * Convenience around `loadModel()` + `new ChatSession(model)` for the
210
+ * common case where a caller just wants an ergonomic session handle.
211
+ *
212
+ * Rejects models that cannot be driven by a `ChatSession`:
213
+ * - Embedding models (`HarrierModel`) have no chat surface.
214
+ * - The native `QianfanOCRModel` exposes callback-based streaming
215
+ * methods that do not structurally satisfy `SessionCapableModel`'s
216
+ * `AsyncGenerator` overloads. The VLM AsyncGenerator wrapper lives
217
+ * in `@mlx-node/vlm` (importing it here would create a circular
218
+ * package dependency), so callers who want a Qianfan-OCR session
219
+ * must import `QianfanOCRModel` from `@mlx-node/vlm` and construct
220
+ * `new ChatSession(model)` directly.
221
+ *
222
+ * `options.draftModelPath` attaches an external draft checkpoint for
223
+ * speculative decoding — gemma4 and dense qwen3_5 only; every other family
224
+ * rejects it.
225
+ * Without the option, Gemma4 loads `<modelPath>/draft/` automatically when
226
+ * that embedded checkpoint is present.
227
+ * Qwen uses the same companion discovery as `loadModel()` unless
228
+ * `autoLoadDraft: false` is passed.
229
+ * The resulting session auto-enables the speculative path when the model
230
+ * reports `hasMtpWeights()` AND does not opt out of the auto-default; pass
231
+ * `enableMtp: false` per call to suppress it, or `enableMtp: true` to force
232
+ * it on a family that opts out. NemotronH opts out (`mtpAutoEnabled()`
233
+ * returns false) because forcing MTP moves the turn into the exclusive lane
234
+ * and out of continuous batching; see `ChatSession.mtpAutoDefaultAllowed`.
235
+ */
236
+ export async function loadSession(
237
+ modelPath: string,
238
+ options?: LoadModelOptions,
239
+ ): Promise<ChatSession<SessionCapableModel>> {
240
+ const modelType = await detectModelType(modelPath);
241
+ const kind = requireFamilyData(modelType).kind;
242
+ if (kind === 'embedding') {
243
+ throw new Error('loadSession: embedding models (Harrier) cannot be wrapped in a ChatSession');
244
+ }
245
+ if (kind === 'vlm') {
246
+ throw new Error(
247
+ 'loadSession: Qianfan-OCR / InternVL session support lives in @mlx-node/vlm. Import QianfanOCRModel from @mlx-node/vlm and construct ChatSession(model) directly.',
248
+ );
249
+ }
250
+ const m = await dispatchLoad(modelType, modelPath, options);
251
+ return new ChatSession(m as unknown as SessionCapableModel);
252
+ }
253
+
254
+ export async function detectModelType(modelPath: string): Promise<ModelType> {
255
+ return detectLocalModelType(modelPath, ggufArchitecture);
256
+ }