@mlx-node/lm 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stream.js CHANGED
@@ -1,48 +1,29 @@
1
- import { Gemma4Model as Gemma4ModelNative, Lfm2Model as Lfm2ModelNative, Qwen3Model as Qwen3ModelNative, Qwen35Model as Qwen35ModelNative, Qwen35MoeModel as Qwen35MoeModelNative, } from '@mlx-node/core';
2
- // Save references to the native callback-based session streaming methods
3
- // before we override them. The legacy `chatStream` surface was removed in
4
- // the chat-session refactor; the remaining session entry points below
5
- // drive all streaming via the `ChatSession` API.
6
- //
7
- // Each wrapper class re-declares these three methods as
8
- // `AsyncGenerator<ChatStreamEvent>` overrides that delegate through the
9
- // shared `_runChatStream` bridge, so the wrapper structurally satisfies
10
- // `SessionCapableModel` and can be passed to `ChatSession<M>`.
11
- // Dense
12
- // oxlint-disable-next-line @typescript-eslint/unbound-method
13
- const _nativeDenseChatStreamSessionStart = Qwen35ModelNative.prototype.chatStreamSessionStart;
14
- // oxlint-disable-next-line @typescript-eslint/unbound-method
15
- const _nativeDenseChatStreamSessionContinue = Qwen35ModelNative.prototype.chatStreamSessionContinue;
16
- // oxlint-disable-next-line @typescript-eslint/unbound-method
17
- const _nativeDenseChatStreamSessionContinueTool = Qwen35ModelNative.prototype.chatStreamSessionContinueTool;
18
- // MoE
19
- // oxlint-disable-next-line @typescript-eslint/unbound-method
20
- const _nativeMoeChatStreamSessionStart = Qwen35MoeModelNative.prototype.chatStreamSessionStart;
21
- // oxlint-disable-next-line @typescript-eslint/unbound-method
22
- const _nativeMoeChatStreamSessionContinue = Qwen35MoeModelNative.prototype.chatStreamSessionContinue;
23
- // oxlint-disable-next-line @typescript-eslint/unbound-method
24
- const _nativeMoeChatStreamSessionContinueTool = Qwen35MoeModelNative.prototype.chatStreamSessionContinueTool;
25
- // LFM2
26
- // oxlint-disable-next-line @typescript-eslint/unbound-method
27
- const _nativeLfm2ChatStreamSessionStart = Lfm2ModelNative.prototype.chatStreamSessionStart;
28
- // oxlint-disable-next-line @typescript-eslint/unbound-method
29
- const _nativeLfm2ChatStreamSessionContinue = Lfm2ModelNative.prototype.chatStreamSessionContinue;
30
- // oxlint-disable-next-line @typescript-eslint/unbound-method
31
- const _nativeLfm2ChatStreamSessionContinueTool = Lfm2ModelNative.prototype.chatStreamSessionContinueTool;
32
- // Gemma4
33
- // oxlint-disable-next-line @typescript-eslint/unbound-method
34
- const _nativeGemma4ChatStreamSessionStart = Gemma4ModelNative.prototype.chatStreamSessionStart;
35
- // oxlint-disable-next-line @typescript-eslint/unbound-method
36
- const _nativeGemma4ChatStreamSessionContinue = Gemma4ModelNative.prototype.chatStreamSessionContinue;
37
- // oxlint-disable-next-line @typescript-eslint/unbound-method
38
- const _nativeGemma4ChatStreamSessionContinueTool = Gemma4ModelNative.prototype.chatStreamSessionContinueTool;
39
- // Qwen3 (legacy, text-only)
40
- // oxlint-disable-next-line @typescript-eslint/unbound-method
41
- const _nativeQwen3ChatStreamSessionStart = Qwen3ModelNative.prototype.chatStreamSessionStart;
42
- // oxlint-disable-next-line @typescript-eslint/unbound-method
43
- const _nativeQwen3ChatStreamSessionContinue = Qwen3ModelNative.prototype.chatStreamSessionContinue;
44
- // oxlint-disable-next-line @typescript-eslint/unbound-method
45
- const _nativeQwen3ChatStreamSessionContinueTool = Qwen3ModelNative.prototype.chatStreamSessionContinueTool;
1
+ import { join } from "node:path";
2
+ import { Gemma4Model as Gemma4ModelNative, Lfm2Model as Lfm2ModelNative, Qwen3Tokenizer, Qwen3Model as Qwen3ModelNative, Qwen35Model as Qwen35ModelNative, Qwen35MoeModel as Qwen35MoeModelNative, } from "@mlx-node/core";
3
+ const modelPathsForTokenizers = new WeakMap();
4
+ const tokenizerPromises = new WeakMap();
5
+ function getNativeIsReasoning(chunk) {
6
+ return typeof chunk.isReasoning === "boolean" ? chunk.isReasoning : undefined;
7
+ }
8
+ function rememberModelPath(model, modelPath) {
9
+ modelPathsForTokenizers.set(model, modelPath);
10
+ }
11
+ async function applyChatTemplateFromModelPath(model, messages, addGenerationPrompt, tools, enableThinking, contentPolicy) {
12
+ const modelPath = modelPathsForTokenizers.get(model);
13
+ if (modelPath == null) {
14
+ throw new Error("applyChatTemplate unavailable: model path was not recorded when this model was loaded");
15
+ }
16
+ let tokenizerPromise = tokenizerPromises.get(model);
17
+ if (tokenizerPromise == null) {
18
+ tokenizerPromise = Qwen3Tokenizer.fromPretrained(join(modelPath, "tokenizer.json"));
19
+ tokenizerPromises.set(model, tokenizerPromise);
20
+ }
21
+ const tokenizer = await tokenizerPromise;
22
+ if (contentPolicy == null) {
23
+ return tokenizer.applyChatTemplate(messages, addGenerationPrompt, tools, enableThinking);
24
+ }
25
+ return tokenizer.applyChatTemplate(messages, addGenerationPrompt, tools, enableThinking, contentPolicy.order, contentPolicy.existingImagePlaceholder);
26
+ }
46
27
  /**
47
28
  * Shared AsyncGenerator adapter for callback-based native streaming methods.
48
29
  *
@@ -149,7 +130,7 @@ export async function* _runChatStream(startCall, signal) {
149
130
  }
150
131
  else {
151
132
  onAbort = triggerAbort;
152
- signal.addEventListener('abort', onAbort, { once: true });
133
+ signal.addEventListener("abort", onAbort, { once: true });
153
134
  }
154
135
  }
155
136
  try {
@@ -171,28 +152,56 @@ export async function* _runChatStream(startCall, signal) {
171
152
  throw item.error;
172
153
  const chunk = item.chunk;
173
154
  if (chunk.done) {
174
- yield {
155
+ if (typeof chunk.thinkingEnabled !== "boolean") {
156
+ throw new Error("Native terminal chat stream chunk is missing thinkingEnabled");
157
+ }
158
+ // The native `ChatStreamChunk` carries `cachedTokens` on the
159
+ // terminal (`done == true`) chunk for every streaming entry
160
+ // point. Emit it on the final event verbatim — undefined means
161
+ // the native dispatch did not populate it (e.g. a bridge-level
162
+ // mock or an in-process driver), in which case downstream
163
+ // consumers treat the absence as "unknown / not plumbed" and
164
+ // skip emitting e.g. `X-Cached-Tokens` rather than reporting a
165
+ // fabricated `0`.
166
+ const chunkWithCached = chunk;
167
+ const finalEvent = {
175
168
  text: chunk.text,
176
169
  done: true,
177
170
  finishReason: chunk.finishReason,
178
171
  toolCalls: chunk.toolCalls ?? [],
179
172
  thinking: chunk.thinking ?? null,
173
+ thinkingEnabled: chunk.thinkingEnabled,
180
174
  numTokens: chunk.numTokens,
181
175
  promptTokens: chunk.promptTokens ?? 0,
182
176
  reasoningTokens: chunk.reasoningTokens ?? 0,
183
177
  rawText: chunk.rawText,
184
178
  performance: chunk.performance ?? undefined,
185
179
  };
180
+ if (typeof chunkWithCached.cachedTokens === "number") {
181
+ finalEvent.cachedTokens = chunkWithCached.cachedTokens;
182
+ }
183
+ if (typeof chunkWithCached.publicRawText === "string") {
184
+ finalEvent.publicRawText = chunkWithCached.publicRawText;
185
+ }
186
+ if (typeof chunkWithCached.textAuthoritative === "boolean") {
187
+ finalEvent.textAuthoritative = chunkWithCached.textAuthoritative;
188
+ }
189
+ yield finalEvent;
186
190
  return;
187
191
  }
188
- yield { text: chunk.text, done: false, isReasoning: chunk.isReasoning ?? undefined };
192
+ const delta = { text: chunk.text, done: false };
193
+ const isReasoning = getNativeIsReasoning(chunk);
194
+ if (isReasoning !== undefined) {
195
+ delta.isReasoning = isReasoning;
196
+ }
197
+ yield delta;
189
198
  }
190
199
  }
191
200
  }
192
201
  finally {
193
202
  if (signal != null && onAbort != null) {
194
203
  try {
195
- signal.removeEventListener('abort', onAbort);
204
+ signal.removeEventListener("abort", onAbort);
196
205
  }
197
206
  catch {
198
207
  // removeEventListener shouldn't throw, but stay defensive —
@@ -203,205 +212,133 @@ export async function* _runChatStream(startCall, signal) {
203
212
  }
204
213
  }
205
214
  /**
206
- * Qwen3.5 dense model with AsyncGenerator-based session streaming.
215
+ * Build the streaming-model subclass for a native chat model class.
207
216
  *
208
- * Streaming is driven through the session API — `chatStreamSessionStart`,
209
- * `chatStreamSessionContinue`, and `chatStreamSessionContinueTool` below
210
- * which adapt the callback-based native methods to
211
- * `AsyncGenerator<ChatStreamEvent>` so the wrapper structurally satisfies
212
- * `SessionCapableModel` and can be passed to `ChatSession<Qwen35Model>`.
213
- */
214
- export class Qwen35Model extends Qwen35ModelNative {
215
- static async load(modelPath) {
216
- const instance = await Qwen35ModelNative.load(modelPath);
217
- Object.setPrototypeOf(instance, Qwen35Model.prototype);
218
- return instance;
219
- }
220
- /**
221
- * Streaming variant of {@link Qwen35Model#chatSessionStart}.
222
- *
223
- * Resets the KV caches, runs the jinja chat template, prefills on
224
- * top of the fresh caches, and streams the decoded reply token-by-
225
- * token. Stops on `<|im_end|>` so the cached history ends on a
226
- * clean ChatML boundary that subsequent `chatStreamSessionContinue`
227
- * deltas can append to. Text-only.
228
- *
229
- * The optional `signal` parameter wires an AbortSignal into the
230
- * `_runChatStream` adapter's fast-abort path. Callers that need
231
- * client-disconnect-aware cancellation (e.g. HTTP endpoints) pass
232
- * one here and the native decode winds down at the next safepoint.
233
- */
234
- // @ts-expect-error — override callback-based native method with AsyncGenerator
235
- async *chatStreamSessionStart(messages, config, signal) {
236
- yield* _runChatStream((callback) => _nativeDenseChatStreamSessionStart.call(this, messages, config ?? null, callback), signal);
237
- }
238
- /**
239
- * Streaming variant of {@link Qwen35Model#chatSessionContinue}.
240
- *
241
- * Builds a raw ChatML delta on top of the live session caches,
242
- * tokenizes it, prefills the delta, and streams the decoded reply.
243
- * Requires a live session started via `chatSessionStart` or
244
- * `chatStreamSessionStart`. Stops on `<|im_end|>`.
245
- *
246
- * `images` is the native opt-in guard parameter — callers that
247
- * attach a new image set must restart the session via
248
- * `chatStreamSessionStart` with the full history. The high-level
249
- * `ChatSession` wrapper handles that routing; callers that drive
250
- * the wrapper directly should pass `null` for text-only continues.
251
- */
252
- // @ts-expect-error — override callback-based native method with AsyncGenerator
253
- async *chatStreamSessionContinue(userMessage, images, config, signal) {
254
- yield* _runChatStream((callback) => _nativeDenseChatStreamSessionContinue.call(this, userMessage, images, config ?? null, callback), signal);
255
- }
256
- /**
257
- * Streaming variant of {@link Qwen35Model#chatSessionContinueTool}.
258
- *
259
- * Builds a ChatML `<tool_response>` delta on top of the live
260
- * session caches and streams the decoded assistant reply. Requires
261
- * a live session started via `chatSessionStart` /
262
- * `chatStreamSessionStart`.
263
- */
264
- // @ts-expect-error — override callback-based native method with AsyncGenerator
265
- async *chatStreamSessionContinueTool(toolCallId, content, config, signal) {
266
- yield* _runChatStream((callback) => _nativeDenseChatStreamSessionContinueTool.call(this, toolCallId, content, config ?? null, callback), signal);
267
- }
268
- }
269
- /**
270
- * Qwen3.5 MoE model wrapper.
217
+ * The returned class:
218
+ * - captures the three native callback-based session-streaming methods
219
+ * from `NativeClass.prototype`,
220
+ * - overrides them as `async *` generators delegating to
221
+ * {@link _runChatStream} with identical argument plumbing (including
222
+ * `config ?? null`, `images`, `isError ?? null`, and the `signal`),
223
+ * - overrides `static load` to re-prototype the native instance onto
224
+ * the concrete subclass (`this`) and optionally record the path,
225
+ * - installs a path-backed `applyChatTemplate` when `opts.applyTemplate`
226
+ * (defaulting to `opts.recordModelPath`).
271
227
  *
272
- * Streaming is driven through the `ChatSession` API overrides below
273
- * adapt the callback-based native methods to
274
- * `AsyncGenerator<ChatStreamEvent>` so the wrapper structurally
275
- * satisfies `SessionCapableModel`.
228
+ * @internal Exported so the VLM wrapper (`@mlx-node/vlm`) builds its
229
+ * `QianfanOCRModel` from the same factory. Not part of the public API.
276
230
  */
277
- export class Qwen35MoeModel extends Qwen35MoeModelNative {
278
- static async load(modelPath) {
279
- const instance = await Qwen35MoeModelNative.load(modelPath);
280
- Object.setPrototypeOf(instance, Qwen35MoeModel.prototype);
281
- return instance;
282
- }
283
- /** Streaming variant of {@link Qwen35MoeModel#chatSessionStart}. */
284
- // @ts-expect-error override callback-based native method with AsyncGenerator
285
- async *chatStreamSessionStart(messages, config, signal) {
286
- yield* _runChatStream((callback) => _nativeMoeChatStreamSessionStart.call(this, messages, config ?? null, callback), signal);
287
- }
288
- /** Streaming variant of {@link Qwen35MoeModel#chatSessionContinue}. */
289
- // @ts-expect-erroroverride callback-based native method with AsyncGenerator
290
- async *chatStreamSessionContinue(userMessage, images, config, signal) {
291
- yield* _runChatStream((callback) => _nativeMoeChatStreamSessionContinue.call(this, userMessage, images, config ?? null, callback), signal);
231
+ export function makeStreamingModel(NativeClass, opts) {
232
+ const recordPath = opts.recordModelPath;
233
+ const applyTemplate = opts.applyTemplate ?? recordPath;
234
+ const templateContentPolicy = opts.templateContentPolicy;
235
+ const replayAssistantRawText = opts.replayAssistantRawText ?? false;
236
+ // Capture the native callback-based methods before the subclass
237
+ // overrides below shadow them on the prototype.
238
+ const nativeStart = NativeClass.prototype.chatStreamSessionStart;
239
+ const nativeContinue = NativeClass.prototype.chatStreamSessionContinue;
240
+ const nativeContinueTool = NativeClass.prototype.chatStreamSessionContinueTool;
241
+ // `NativeClass` is structurally a constructor; cast to a concrete
242
+ // constructor type so `class extends` accepts it. Runtime behavior is
243
+ // unchangedwe extend the real native class.
244
+ const Base = NativeClass;
245
+ class StreamingModelImpl extends Base {
246
+ supportsReplayReasoningCapture() {
247
+ return true;
248
+ }
249
+ replaysAssistantRawText() {
250
+ return replayAssistantRawText;
251
+ }
252
+ static async load(modelPath, ...rest) {
253
+ // Forward any trailing family-specific load options verbatim (e.g.
254
+ // Gemma4's `Gemma4LoadOptions` with `draftModelPath`); families whose
255
+ // native `load` takes only the path receive no extras. The public
256
+ // signature is re-narrowed per family via `Parameters<C['load']>` in
257
+ // the factory return type below.
258
+ const instance = await NativeClass.load(modelPath, ...rest);
259
+ // Use `this.prototype` (not `StreamingModelImpl.prototype`) so the
260
+ // concrete subclass declared per family supplies the prototype and
261
+ // `instanceof ConcreteSubclass` holds.
262
+ Object.setPrototypeOf(instance, this.prototype);
263
+ if (recordPath)
264
+ rememberModelPath(instance, modelPath);
265
+ return instance;
266
+ }
267
+ // The native methods are callback-based, but `Base` is typed as a
268
+ // `SessionCapableModel` constructor (whose streaming methods already
269
+ // return `AsyncGenerator<ChatStreamEvent>`), so these overrides are
270
+ // type-compatible and need no `@ts-expect-error` suppression. The
271
+ // callback bridging happens at runtime via the captured natives.
272
+ async *chatStreamSessionStart(messages, config, signal) {
273
+ yield* _runChatStream((callback) => nativeStart.call(this, messages, (config ?? null), callback), signal);
274
+ }
275
+ async *chatStreamSessionContinue(messages, config, signal) {
276
+ yield* _runChatStream((callback) => nativeContinue.call(this, messages, (config ?? null), callback), signal);
277
+ }
278
+ async *chatStreamSessionContinueTool(messages, config, signal) {
279
+ yield* _runChatStream((callback) => nativeContinueTool.call(this, messages, (config ?? null), callback), signal);
280
+ }
292
281
  }
293
- /** Streaming variant of {@link Qwen35MoeModel#chatSessionContinueTool}. */
294
- // @ts-expect-error — override callback-based native method with AsyncGenerator
295
- async *chatStreamSessionContinueTool(toolCallId, content, config, signal) {
296
- yield* _runChatStream((callback) => _nativeMoeChatStreamSessionContinueTool.call(this, toolCallId, content, config ?? null, callback), signal);
282
+ if (applyTemplate) {
283
+ Object.defineProperty(StreamingModelImpl.prototype, "applyChatTemplate", {
284
+ configurable: true,
285
+ writable: true,
286
+ value(messages, addGenerationPrompt, tools, enableThinking) {
287
+ return applyChatTemplateFromModelPath(this, messages, addGenerationPrompt, tools, enableThinking, templateContentPolicy);
288
+ },
289
+ });
297
290
  }
291
+ return StreamingModelImpl;
298
292
  }
299
293
  /**
300
- * LFM2 model wrapper.
294
+ * Qwen3.5 dense model with AsyncGenerator-based session streaming.
301
295
  *
302
- * Streaming is driven through the `ChatSession` API overrides below
303
- * adapt the callback-based native methods to
304
- * `AsyncGenerator<ChatStreamEvent>` so the wrapper structurally
305
- * satisfies `SessionCapableModel`. LFM2 is text-only; the native
306
- * `images` guard rejects non-empty image sets with an
307
- * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` prefix.
296
+ * The empty `extends` inherits the factory's streaming overrides,
297
+ * `static load`, and `applyChatTemplate`, and supplies the concrete
298
+ * `.name === 'Qwen35Model'` and a working `instanceof`. Records its
299
+ * model path so `applyChatTemplate` can serve a lazily built tokenizer.
308
300
  */
309
- export class Lfm2Model extends Lfm2ModelNative {
310
- static async load(modelPath) {
311
- const instance = await Lfm2ModelNative.load(modelPath);
312
- Object.setPrototypeOf(instance, Lfm2Model.prototype);
313
- return instance;
314
- }
315
- /** Streaming variant of {@link Lfm2Model#chatSessionStart}. */
316
- // @ts-expect-error — override callback-based native method with AsyncGenerator
317
- async *chatStreamSessionStart(messages, config, signal) {
318
- yield* _runChatStream((callback) => _nativeLfm2ChatStreamSessionStart.call(this, messages, config ?? null, callback), signal);
319
- }
320
- /** Streaming variant of {@link Lfm2Model#chatSessionContinue}. */
321
- // @ts-expect-error — override callback-based native method with AsyncGenerator
322
- async *chatStreamSessionContinue(userMessage, images, config, signal) {
323
- yield* _runChatStream((callback) => _nativeLfm2ChatStreamSessionContinue.call(this, userMessage, images, config ?? null, callback), signal);
324
- }
325
- /** Streaming variant of {@link Lfm2Model#chatSessionContinueTool}. */
326
- // @ts-expect-error — override callback-based native method with AsyncGenerator
327
- async *chatStreamSessionContinueTool(toolCallId, content, config, signal) {
328
- yield* _runChatStream((callback) => _nativeLfm2ChatStreamSessionContinueTool.call(this, toolCallId, content, config ?? null, callback), signal);
329
- }
301
+ export class Qwen35Model extends makeStreamingModel(Qwen35ModelNative, {
302
+ recordModelPath: true,
303
+ }) {
330
304
  }
331
- /**
332
- * Gemma4 model wrapper.
333
- *
334
- * Streaming is driven through the `ChatSession` API — overrides below
335
- * adapt the callback-based native methods to
336
- * `AsyncGenerator<ChatStreamEvent>` so the wrapper structurally
337
- * satisfies `SessionCapableModel`. Gemma4 is text-only in the
338
- * current refactor scope; the native `images` guard rejects non-empty
339
- * image sets with an `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` prefix.
340
- */
341
- export class Gemma4Model extends Gemma4ModelNative {
342
- static async load(modelPath) {
343
- const instance = await Gemma4ModelNative.load(modelPath);
344
- Object.setPrototypeOf(instance, Gemma4Model.prototype);
345
- return instance;
346
- }
347
- /** Streaming variant of {@link Gemma4Model#chatSessionStart}. */
348
- // @ts-expect-error — override callback-based native method with AsyncGenerator
349
- async *chatStreamSessionStart(messages, config, signal) {
350
- yield* _runChatStream((callback) => _nativeGemma4ChatStreamSessionStart.call(this, messages, config ?? null, callback), signal);
351
- }
352
- /** Streaming variant of {@link Gemma4Model#chatSessionContinue}. */
353
- // @ts-expect-error — override callback-based native method with AsyncGenerator
354
- async *chatStreamSessionContinue(userMessage, images, config, signal) {
355
- yield* _runChatStream((callback) => _nativeGemma4ChatStreamSessionContinue.call(this, userMessage, images, config ?? null, callback), signal);
356
- }
357
- /** Streaming variant of {@link Gemma4Model#chatSessionContinueTool}. */
358
- // @ts-expect-error — override callback-based native method with AsyncGenerator
359
- async *chatStreamSessionContinueTool(toolCallId, content, config, signal) {
360
- yield* _runChatStream((callback) => _nativeGemma4ChatStreamSessionContinueTool.call(this, toolCallId, content, config ?? null, callback), signal);
361
- }
305
+ /** Qwen3.5 MoE model — see {@link Qwen35Model} for the wrapper shape. */
306
+ export class Qwen35MoeModel extends makeStreamingModel(Qwen35MoeModelNative, {
307
+ recordModelPath: true,
308
+ }) {
309
+ }
310
+ /** LFM2 model (text-only) — see {@link Qwen35Model} for the wrapper shape. */
311
+ export class Lfm2Model extends makeStreamingModel(Lfm2ModelNative, {
312
+ recordModelPath: true,
313
+ replayAssistantRawText: true,
314
+ }) {
315
+ }
316
+ /** Gemma4 model (text-only) — see {@link Qwen35Model} for the wrapper shape. */
317
+ export class Gemma4Model extends makeStreamingModel(Gemma4ModelNative, {
318
+ recordModelPath: true,
319
+ }) {
362
320
  }
363
321
  /**
364
- * Qwen3 (legacy) model wrapper.
322
+ * Qwen3 (first-gen, text-only) model.
365
323
  *
366
- * Streaming is driven through the `ChatSession` API overrides below
367
- * adapt the callback-based native methods to
368
- * `AsyncGenerator<ChatStreamEvent>` so the wrapper structurally
369
- * satisfies `SessionCapableModel`. Qwen3 legacy is text-only; the
370
- * native `images` guard rejects non-empty image sets with an
371
- * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` prefix.
324
+ * Records its model path (so prototype-set + path-recording match the
325
+ * other families) but does not install the factory's path-backed
326
+ * `applyChatTemplate`; it retains the native tokenizer-backed method.
372
327
  */
373
- export class Qwen3Model extends Qwen3ModelNative {
374
- static async load(modelPath) {
375
- const instance = await Qwen3ModelNative.load(modelPath);
376
- Object.setPrototypeOf(instance, Qwen3Model.prototype);
377
- return instance;
378
- }
379
- /** Streaming variant of {@link Qwen3Model#chatSessionStart}. */
380
- // @ts-expect-error — override callback-based native method with AsyncGenerator
381
- async *chatStreamSessionStart(messages, config, signal) {
382
- yield* _runChatStream((callback) => _nativeQwen3ChatStreamSessionStart.call(this, messages, config ?? null, callback), signal);
383
- }
384
- /** Streaming variant of {@link Qwen3Model#chatSessionContinue}. */
385
- // @ts-expect-error — override callback-based native method with AsyncGenerator
386
- async *chatStreamSessionContinue(userMessage, images, config, signal) {
387
- yield* _runChatStream((callback) => _nativeQwen3ChatStreamSessionContinue.call(this, userMessage, images, config ?? null, callback), signal);
388
- }
389
- /** Streaming variant of {@link Qwen3Model#chatSessionContinueTool}. */
390
- // @ts-expect-error — override callback-based native method with AsyncGenerator
391
- async *chatStreamSessionContinueTool(toolCallId, content, config, signal) {
392
- yield* _runChatStream((callback) => _nativeQwen3ChatStreamSessionContinueTool.call(this, toolCallId, content, config ?? null, callback), signal);
393
- }
328
+ export class Qwen3Model extends makeStreamingModel(Qwen3ModelNative, {
329
+ recordModelPath: true,
330
+ applyTemplate: false,
331
+ }) {
394
332
  }
395
333
  // -------------------------------------------------------------------
396
334
  // Compile-time conformance check
397
335
  // -------------------------------------------------------------------
398
336
  //
399
- // Ensures each wrapper class structurally satisfies
400
- // `SessionCapableModel` so `ChatSession<XxxModel>` will type-check in
401
- // downstream code. The assignments are compile-only — the
402
- // `null as unknown as T` placeholder never runs. If a wrapper's
403
- // override signature drifts away from the interface, TypeScript will
404
- // fail to compile this block, surfacing the regression at build time.
337
+ // Ensures each family class structurally satisfies
338
+ // `SessionCapableModel` so `ChatSession<XxxModel>` type-checks in
339
+ // downstream code. Compile-only — the `null as unknown as T`
340
+ // placeholder never runs. If a factory override signature drifts away
341
+ // from the interface, this block fails to compile.
405
342
  function _assertSessionCapable() {
406
343
  const _qwen35 = null;
407
344
  const _moe = null;
@@ -415,3 +352,29 @@ function _assertSessionCapable() {
415
352
  void _qwen3;
416
353
  }
417
354
  void _assertSessionCapable;
355
+ /** Compile-time guard that both Qwen3.5 native classes and wrappers retain the exact media planner. */
356
+ function _assertExpandedPromptPlannerSurfaces() {
357
+ const _nativeDense = null;
358
+ const _nativeMoe = null;
359
+ const _wrappedDense = null;
360
+ const _wrappedMoe = null;
361
+ void _nativeDense;
362
+ void _nativeMoe;
363
+ void _wrappedDense;
364
+ void _wrappedMoe;
365
+ }
366
+ void _assertExpandedPromptPlannerSurfaces;
367
+ /** Compile-time guard that the factory preserves every non-streaming native member. */
368
+ function _assertPreservedNativeSurfaces() {
369
+ const _qwen3 = null;
370
+ const _qwen35 = null;
371
+ const _moe = null;
372
+ const _lfm2 = null;
373
+ const _gemma4 = null;
374
+ void _qwen3;
375
+ void _qwen35;
376
+ void _moe;
377
+ void _lfm2;
378
+ void _gemma4;
379
+ }
380
+ void _assertPreservedNativeSurfaces;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mlx-node/lm",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "homepage": "https://github.com/mlx-node/mlx-node",
5
5
  "bugs": {
6
6
  "url": "https://github.com/mlx-node/mlx-node/issues"
@@ -28,9 +28,9 @@
28
28
  "test": "vite test run"
29
29
  },
30
30
  "dependencies": {
31
- "@mlx-node/core": "0.0.7"
31
+ "@mlx-node/core": "0.0.9"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "^25.6.0"
34
+ "@types/node": "^26.0.0"
35
35
  }
36
36
  }