@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.
- package/dist/chat-session.d.ts +1 -1
- package/dist/chat-session.d.ts.map +1 -1
- package/dist/chat-session.js +2 -2
- package/dist/draft-companion.d.ts +16 -0
- package/dist/draft-companion.d.ts.map +1 -0
- package/dist/draft-companion.js +76 -0
- package/dist/family-data.d.ts +2 -0
- package/dist/family-data.d.ts.map +1 -1
- package/dist/family-data.js +2 -0
- package/dist/gguf-metadata.d.ts +2 -0
- package/dist/gguf-metadata.d.ts.map +1 -0
- package/dist/gguf-metadata.js +128 -0
- package/dist/model-detection.d.ts +6 -0
- package/dist/model-detection.d.ts.map +1 -0
- package/dist/model-detection.js +38 -0
- package/dist/model-discovery.d.ts +24 -0
- package/dist/model-discovery.d.ts.map +1 -0
- package/dist/model-discovery.js +274 -0
- package/dist/models/model-loader.d.ts +6 -0
- package/dist/models/model-loader.d.ts.map +1 -1
- package/dist/models/model-loader.js +10 -32
- package/dist/models/paged-config-override.d.ts.map +1 -1
- package/dist/models/paged-config-override.js +21 -1
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +5 -5
- package/package.json +21 -3
- package/src/chat-session.ts +2369 -0
- package/src/draft-companion.ts +74 -0
- package/src/family-data.ts +542 -0
- package/src/gguf-metadata.ts +117 -0
- package/src/index.ts +151 -0
- package/src/model-detection.ts +46 -0
- package/src/model-discovery.ts +329 -0
- package/src/models/lfm2-configs.ts +110 -0
- package/src/models/model-loader.ts +256 -0
- package/src/models/paged-config-override.ts +387 -0
- package/src/models/qwen3-configs.ts +113 -0
- package/src/models/qwen3_5-configs.ts +60 -0
- package/src/profiling.ts +69 -0
- package/src/stream.ts +960 -0
- package/src/tools/index.ts +58 -0
- package/src/tools/types.ts +215 -0
package/src/stream.ts
ADDED
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Gemma4Model as Gemma4ModelNative,
|
|
5
|
+
Lfm2Model as Lfm2ModelNative,
|
|
6
|
+
MuseGlimmerModel as MuseGlimmerModelNative,
|
|
7
|
+
NemotronHModel as NemotronHModelNative,
|
|
8
|
+
Qwen3Tokenizer,
|
|
9
|
+
Qwen3Model as Qwen3ModelNative,
|
|
10
|
+
Qwen35Model as Qwen35ModelNative,
|
|
11
|
+
Qwen35MoeModel as Qwen35MoeModelNative,
|
|
12
|
+
} from "@mlx-node/core";
|
|
13
|
+
import type {
|
|
14
|
+
ChatConfig,
|
|
15
|
+
ChatMessage,
|
|
16
|
+
ChatResult,
|
|
17
|
+
ChatStreamChunk,
|
|
18
|
+
ChatStreamHandle,
|
|
19
|
+
PerformanceMetrics,
|
|
20
|
+
ToolDefinition,
|
|
21
|
+
ToolCallResult,
|
|
22
|
+
} from "@mlx-node/core";
|
|
23
|
+
|
|
24
|
+
import type { SessionCapableModel } from "./chat-session.js";
|
|
25
|
+
|
|
26
|
+
interface NativeChatSessionCall {
|
|
27
|
+
cancel(): void;
|
|
28
|
+
result(): Promise<ChatResult>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface NativeChatSessionOperations {
|
|
32
|
+
chatSessionStart?(
|
|
33
|
+
messages: ChatMessage[],
|
|
34
|
+
config?: ChatConfig | null,
|
|
35
|
+
): Promise<ChatResult>;
|
|
36
|
+
chatSessionContinue?(
|
|
37
|
+
messages: ChatMessage[],
|
|
38
|
+
config?: ChatConfig | null,
|
|
39
|
+
): Promise<ChatResult>;
|
|
40
|
+
chatSessionContinueTool?(
|
|
41
|
+
messages: ChatMessage[],
|
|
42
|
+
config?: ChatConfig | null,
|
|
43
|
+
): Promise<ChatResult>;
|
|
44
|
+
beginChatSessionStart?(
|
|
45
|
+
messages: ChatMessage[],
|
|
46
|
+
config?: ChatConfig | null,
|
|
47
|
+
): Promise<NativeChatSessionCall>;
|
|
48
|
+
beginChatSessionContinue?(
|
|
49
|
+
messages: ChatMessage[],
|
|
50
|
+
config?: ChatConfig | null,
|
|
51
|
+
): Promise<NativeChatSessionCall>;
|
|
52
|
+
beginChatSessionContinueTool?(
|
|
53
|
+
messages: ChatMessage[],
|
|
54
|
+
config?: ChatConfig | null,
|
|
55
|
+
): Promise<NativeChatSessionCall>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ChatStreamDelta {
|
|
59
|
+
text: string;
|
|
60
|
+
done: false;
|
|
61
|
+
isReasoning?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ChatStreamFinal {
|
|
65
|
+
text: string;
|
|
66
|
+
done: true;
|
|
67
|
+
finishReason: string;
|
|
68
|
+
toolCalls: ToolCallResult[];
|
|
69
|
+
thinking: string | null;
|
|
70
|
+
/** Effective `enable_thinking` value passed to the model chat template. */
|
|
71
|
+
thinkingEnabled: boolean;
|
|
72
|
+
numTokens: number;
|
|
73
|
+
promptTokens: number;
|
|
74
|
+
reasoningTokens: number;
|
|
75
|
+
rawText: string;
|
|
76
|
+
/**
|
|
77
|
+
* Native token-aware reasoning-redacted raw output. ChatSession uses this
|
|
78
|
+
* when it captures full reasoning internally for deterministic replay while
|
|
79
|
+
* keeping `includeReasoning: false` private to the caller.
|
|
80
|
+
*/
|
|
81
|
+
publicRawText?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Whether terminal `text` is the complete parsed assistant content.
|
|
84
|
+
* Gemma emits visible content exclusively as deltas and sets false.
|
|
85
|
+
*/
|
|
86
|
+
textAuthoritative?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Number of prompt tokens served from the reused KV-cache prefix on
|
|
89
|
+
* this turn. Mirrors the `cachedTokens` field on the non-streaming
|
|
90
|
+
* `ChatResult` so session-aware streaming consumers can observe
|
|
91
|
+
* prefix-cache reuse without round-tripping to the non-streaming
|
|
92
|
+
* path.
|
|
93
|
+
*
|
|
94
|
+
* The native `ChatStreamChunk` surfaces `cachedTokens` on the
|
|
95
|
+
* terminal (`done == true`) chunk for every streaming entry point
|
|
96
|
+
* (Qwen3, Qwen3.5 Dense / MoE, LFM2, Gemma4, QianfanOCR) — start-path
|
|
97
|
+
* chunks carry the matched prefix length from
|
|
98
|
+
* `verify_cache_prefix_direct`, delta-path chunks carry the reused
|
|
99
|
+
* prior-history length. Non-terminal deltas carry `None` /
|
|
100
|
+
* `undefined` (only the terminal chunk is authoritative).
|
|
101
|
+
*
|
|
102
|
+
* This field remains OPTIONAL because the bridge-level mock tests
|
|
103
|
+
* (and any future in-process driver that constructs its own
|
|
104
|
+
* `ChatStreamChunk`) may legitimately omit it. Consumers SHOULD
|
|
105
|
+
* treat `undefined` distinctly from `0` (e.g. skip emitting
|
|
106
|
+
* `X-Cached-Tokens` rather than reporting `0`); a numeric value is
|
|
107
|
+
* always authoritative.
|
|
108
|
+
*/
|
|
109
|
+
cachedTokens?: number;
|
|
110
|
+
performance?: PerformanceMetrics;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type ChatStreamEvent = ChatStreamDelta | ChatStreamFinal;
|
|
114
|
+
|
|
115
|
+
const modelPathsForTokenizers = new WeakMap<object, string>();
|
|
116
|
+
const tokenizerPromises = new WeakMap<object, Promise<Qwen3Tokenizer>>();
|
|
117
|
+
|
|
118
|
+
type TemplateContentOrder = "textThenMedia" | "imagesThenText";
|
|
119
|
+
|
|
120
|
+
interface TemplateContentPolicy {
|
|
121
|
+
order: TemplateContentOrder;
|
|
122
|
+
/**
|
|
123
|
+
* When sanitized text already contains this model-owned placeholder, keep
|
|
124
|
+
* the message structured but do not synthesize additional image parts.
|
|
125
|
+
*/
|
|
126
|
+
existingImagePlaceholder?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface PolicyAwareTokenizer {
|
|
130
|
+
applyChatTemplate(
|
|
131
|
+
messages: ChatMessage[],
|
|
132
|
+
addGenerationPrompt?: boolean | null,
|
|
133
|
+
tools?: ToolDefinition[] | null,
|
|
134
|
+
enableThinking?: boolean | null,
|
|
135
|
+
contentOrder?: TemplateContentOrder | null,
|
|
136
|
+
existingImagePlaceholder?: string | null,
|
|
137
|
+
reasoningEffort?: string | null,
|
|
138
|
+
): Promise<Uint32Array>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getNativeIsReasoning(chunk: ChatStreamChunk): boolean | undefined {
|
|
142
|
+
return typeof chunk.isReasoning === "boolean" ? chunk.isReasoning : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function applyChatTemplateFromModelPath(
|
|
146
|
+
model: object,
|
|
147
|
+
messages: ChatMessage[],
|
|
148
|
+
addGenerationPrompt?: boolean | null,
|
|
149
|
+
tools?: ToolDefinition[] | null,
|
|
150
|
+
enableThinking?: boolean | null,
|
|
151
|
+
contentPolicy?: TemplateContentPolicy,
|
|
152
|
+
reasoningEffort?: string | null,
|
|
153
|
+
): Promise<Uint32Array> {
|
|
154
|
+
const modelPath = modelPathsForTokenizers.get(model);
|
|
155
|
+
if (modelPath == null) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"applyChatTemplate unavailable: model path was not recorded when this model was loaded",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
let tokenizerPromise = tokenizerPromises.get(model);
|
|
161
|
+
if (tokenizerPromise == null) {
|
|
162
|
+
tokenizerPromise = Qwen3Tokenizer.fromPretrained(
|
|
163
|
+
join(modelPath, "tokenizer.json"),
|
|
164
|
+
);
|
|
165
|
+
tokenizerPromises.set(model, tokenizerPromise);
|
|
166
|
+
}
|
|
167
|
+
const tokenizer = await tokenizerPromise;
|
|
168
|
+
if (contentPolicy == null) {
|
|
169
|
+
return tokenizer.applyChatTemplate(
|
|
170
|
+
messages,
|
|
171
|
+
addGenerationPrompt,
|
|
172
|
+
tools,
|
|
173
|
+
enableThinking,
|
|
174
|
+
undefined,
|
|
175
|
+
undefined,
|
|
176
|
+
reasoningEffort,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return (tokenizer as PolicyAwareTokenizer).applyChatTemplate(
|
|
180
|
+
messages,
|
|
181
|
+
addGenerationPrompt,
|
|
182
|
+
tools,
|
|
183
|
+
enableThinking,
|
|
184
|
+
contentPolicy.order,
|
|
185
|
+
contentPolicy.existingImagePlaceholder,
|
|
186
|
+
reasoningEffort,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Shared AsyncGenerator adapter for callback-based native streaming methods.
|
|
192
|
+
*
|
|
193
|
+
* Takes a `startCall` closure that, given the JS-side callback, dispatches
|
|
194
|
+
* the underlying native stream (whatever method signature that is — the
|
|
195
|
+
* closure captures `messages` / `config` / `userMessage` etc) and resolves
|
|
196
|
+
* with a `ChatStreamHandle`. The generator pumps the resulting chunk queue,
|
|
197
|
+
* transforms each chunk into a `ChatStreamEvent`, and calls `handle.cancel()`
|
|
198
|
+
* in a `finally` block so early termination (user `break`, exception) still
|
|
199
|
+
* cleans up native state.
|
|
200
|
+
*
|
|
201
|
+
* ## Signal-driven fast-abort
|
|
202
|
+
*
|
|
203
|
+
* When an optional `AbortSignal` is supplied and fires, the adapter:
|
|
204
|
+
* 1. Calls `handle.cancel()` immediately so the native decode stops
|
|
205
|
+
* on the next safepoint rather than running to completion.
|
|
206
|
+
* 2. Wakes the pending `waitForItem()` await by pushing a synthetic
|
|
207
|
+
* "aborted" marker into the queue and calling `notify()`. Without
|
|
208
|
+
* this wake-up the generator would stay parked on the `await`
|
|
209
|
+
* until the next native chunk arrived, which on a fast-abort
|
|
210
|
+
* path (client disconnect before first token) never happens.
|
|
211
|
+
* 3. The generator sees the marker, breaks out of its loop, and the
|
|
212
|
+
* finally block runs `cancelOnce()` — which is a no-op because
|
|
213
|
+
* `triggerAbort` already flipped the `cancelled` flag. Some
|
|
214
|
+
* backends throw on double-cancel, so routing every cancel site
|
|
215
|
+
* through `cancelOnce` keeps abort behavior deterministic.
|
|
216
|
+
*
|
|
217
|
+
* The finally block is also the landing site for the consumer calling
|
|
218
|
+
* `.return()` on the outer generator — the existing `yield` cleanup
|
|
219
|
+
* covers that case. Signal-driven abort covers the window where the
|
|
220
|
+
* consumer cannot reach `.return()` because they are blocked waiting
|
|
221
|
+
* for the very `yield` that `waitForItem()` is gating.
|
|
222
|
+
*
|
|
223
|
+
* @internal Exported so the VLM wrapper (`@mlx-node/vlm`) can reuse the
|
|
224
|
+
* exact same bridge without duplicating the plumbing. Not part of the
|
|
225
|
+
* public API — may change without notice.
|
|
226
|
+
*/
|
|
227
|
+
export async function* _runChatStream(
|
|
228
|
+
startCall: (
|
|
229
|
+
callback: (err: Error | null, chunk: ChatStreamChunk) => void,
|
|
230
|
+
) => Promise<ChatStreamHandle>,
|
|
231
|
+
signal?: AbortSignal,
|
|
232
|
+
): AsyncGenerator<ChatStreamEvent> {
|
|
233
|
+
// The native ThreadsafeFunction uses the same fixed ceiling. This JS-side
|
|
234
|
+
// guard also protects handwritten/test adapters that bypass the Rust glue.
|
|
235
|
+
const maxBufferedEvents = 64;
|
|
236
|
+
const queue: Array<{
|
|
237
|
+
chunk?: ChatStreamChunk;
|
|
238
|
+
error?: Error;
|
|
239
|
+
aborted?: boolean;
|
|
240
|
+
}> = [];
|
|
241
|
+
let resolve: (() => void) | null = null;
|
|
242
|
+
let handle: ChatStreamHandle | null = null;
|
|
243
|
+
let cancelRequested = false;
|
|
244
|
+
let cancelled = false;
|
|
245
|
+
let overflowed = false;
|
|
246
|
+
|
|
247
|
+
const cancelOnce = (): void => {
|
|
248
|
+
if (cancelled) return;
|
|
249
|
+
if (handle === null) {
|
|
250
|
+
cancelRequested = true;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
cancelled = true;
|
|
254
|
+
try {
|
|
255
|
+
handle.cancel();
|
|
256
|
+
} catch {
|
|
257
|
+
// Cancellation is best-effort. The distinguished backlog error below
|
|
258
|
+
// remains authoritative for the consumer even if a backend throws.
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const waitForItem = () =>
|
|
263
|
+
queue.length > 0
|
|
264
|
+
? Promise.resolve()
|
|
265
|
+
: new Promise<void>((r) => {
|
|
266
|
+
resolve = r;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const notify = () => {
|
|
270
|
+
if (resolve) {
|
|
271
|
+
const r = resolve;
|
|
272
|
+
resolve = null;
|
|
273
|
+
r();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const callback = (err: Error | null, chunk: ChatStreamChunk) => {
|
|
278
|
+
if (overflowed) return;
|
|
279
|
+
if (queue.length >= maxBufferedEvents) {
|
|
280
|
+
overflowed = true;
|
|
281
|
+
cancelOnce();
|
|
282
|
+
// Keep the queue at its fixed ceiling while guaranteeing the consumer
|
|
283
|
+
// eventually observes why the stream was cancelled.
|
|
284
|
+
queue[queue.length - 1] = {
|
|
285
|
+
error: new Error(
|
|
286
|
+
`Native chat stream backlog exceeded ${maxBufferedEvents} buffered events`,
|
|
287
|
+
),
|
|
288
|
+
};
|
|
289
|
+
notify();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
queue.push(err ? { error: err } : { chunk });
|
|
293
|
+
notify();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
handle = await startCall(callback);
|
|
297
|
+
if (cancelRequested) cancelOnce();
|
|
298
|
+
|
|
299
|
+
// Guard against double-cancel. Some native backends throw on a
|
|
300
|
+
// second `cancel()`; we route every cancel site through this
|
|
301
|
+
// helper so the abort path (via `triggerAbort`) and the unwind
|
|
302
|
+
// path (via the `finally` block) don't cancel twice and so any
|
|
303
|
+
// backend that does throw is swallowed rather than escaping as
|
|
304
|
+
// an error out of an otherwise-clean early termination.
|
|
305
|
+
// Signal-driven fast-abort. If the signal is already aborted at
|
|
306
|
+
// attach time we still arm the listener so the synchronous abort
|
|
307
|
+
// dispatch path runs below (calling `handle.cancel()` after the
|
|
308
|
+
// handle is in hand, not before — there is nothing to cancel
|
|
309
|
+
// pre-start). For an already-fired signal Node delivers the
|
|
310
|
+
// `'abort'` event on the next microtask.
|
|
311
|
+
let onAbort: (() => void) | null = null;
|
|
312
|
+
if (signal != null) {
|
|
313
|
+
const triggerAbort = (): void => {
|
|
314
|
+
// Cancel the native side first so any work-in-flight winds
|
|
315
|
+
// down ASAP. `cancelOnce` is idempotent — the finally block
|
|
316
|
+
// will invoke it again after the generator unwinds, but the
|
|
317
|
+
// second call becomes a no-op.
|
|
318
|
+
cancelOnce();
|
|
319
|
+
// Push a synthetic abort marker so the consumer-visible
|
|
320
|
+
// generator breaks out of its loop at the next iteration
|
|
321
|
+
// rather than waiting for a native chunk that will never
|
|
322
|
+
// arrive (e.g. client disconnect before first token).
|
|
323
|
+
queue.push({ aborted: true });
|
|
324
|
+
notify();
|
|
325
|
+
};
|
|
326
|
+
if (signal.aborted) {
|
|
327
|
+
// Signal already fired before we could attach — dispatch
|
|
328
|
+
// synchronously so the waitForItem below resolves immediately.
|
|
329
|
+
triggerAbort();
|
|
330
|
+
} else {
|
|
331
|
+
onAbort = triggerAbort;
|
|
332
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
loop: while (true) {
|
|
338
|
+
await waitForItem();
|
|
339
|
+
while (queue.length > 0) {
|
|
340
|
+
const item = queue.shift()!;
|
|
341
|
+
if (item.aborted) {
|
|
342
|
+
// Consumer asked us to stop before the next chunk landed.
|
|
343
|
+
// Break out so the finally block runs `handle.cancel()`
|
|
344
|
+
// (idempotent — `triggerAbort` already called it) and the
|
|
345
|
+
// consumer-side `for await` unblocks cleanly. We do NOT
|
|
346
|
+
// throw an AbortError: callers (e.g. server endpoints that
|
|
347
|
+
// flag client disconnect) have already decided this is not
|
|
348
|
+
// an error from their perspective, just an early stop.
|
|
349
|
+
break loop;
|
|
350
|
+
}
|
|
351
|
+
if (item.error) throw item.error;
|
|
352
|
+
const chunk = item.chunk!;
|
|
353
|
+
if (chunk.done) {
|
|
354
|
+
if (typeof chunk.thinkingEnabled !== "boolean") {
|
|
355
|
+
throw new Error("Native terminal chat stream chunk is missing thinkingEnabled");
|
|
356
|
+
}
|
|
357
|
+
// The native `ChatStreamChunk` carries `cachedTokens` on the
|
|
358
|
+
// terminal (`done == true`) chunk for every streaming entry
|
|
359
|
+
// point. Emit it on the final event verbatim — undefined means
|
|
360
|
+
// the native dispatch did not populate it (e.g. a bridge-level
|
|
361
|
+
// mock or an in-process driver), in which case downstream
|
|
362
|
+
// consumers treat the absence as "unknown / not plumbed" and
|
|
363
|
+
// skip emitting e.g. `X-Cached-Tokens` rather than reporting a
|
|
364
|
+
// fabricated `0`.
|
|
365
|
+
const chunkWithCached = chunk as ChatStreamChunk & {
|
|
366
|
+
cachedTokens?: number;
|
|
367
|
+
publicRawText?: string;
|
|
368
|
+
textAuthoritative?: boolean;
|
|
369
|
+
};
|
|
370
|
+
const finalEvent: ChatStreamFinal = {
|
|
371
|
+
text: chunk.text,
|
|
372
|
+
done: true,
|
|
373
|
+
finishReason: chunk.finishReason!,
|
|
374
|
+
toolCalls: chunk.toolCalls ?? [],
|
|
375
|
+
thinking: chunk.thinking ?? null,
|
|
376
|
+
thinkingEnabled: chunk.thinkingEnabled,
|
|
377
|
+
numTokens: chunk.numTokens!,
|
|
378
|
+
promptTokens: chunk.promptTokens ?? 0,
|
|
379
|
+
reasoningTokens: chunk.reasoningTokens ?? 0,
|
|
380
|
+
rawText: chunk.rawText!,
|
|
381
|
+
performance: chunk.performance ?? undefined,
|
|
382
|
+
};
|
|
383
|
+
if (typeof chunkWithCached.cachedTokens === "number") {
|
|
384
|
+
finalEvent.cachedTokens = chunkWithCached.cachedTokens;
|
|
385
|
+
}
|
|
386
|
+
if (typeof chunkWithCached.publicRawText === "string") {
|
|
387
|
+
finalEvent.publicRawText = chunkWithCached.publicRawText;
|
|
388
|
+
}
|
|
389
|
+
if (typeof chunkWithCached.textAuthoritative === "boolean") {
|
|
390
|
+
finalEvent.textAuthoritative = chunkWithCached.textAuthoritative;
|
|
391
|
+
}
|
|
392
|
+
yield finalEvent;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const delta: ChatStreamDelta = { text: chunk.text, done: false };
|
|
396
|
+
const isReasoning = getNativeIsReasoning(chunk);
|
|
397
|
+
if (isReasoning !== undefined) {
|
|
398
|
+
delta.isReasoning = isReasoning;
|
|
399
|
+
}
|
|
400
|
+
yield delta;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} finally {
|
|
404
|
+
if (signal != null && onAbort != null) {
|
|
405
|
+
try {
|
|
406
|
+
signal.removeEventListener("abort", onAbort);
|
|
407
|
+
} catch {
|
|
408
|
+
// removeEventListener shouldn't throw, but stay defensive —
|
|
409
|
+
// a misbehaving signal must not leak out of the finally.
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
cancelOnce();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Translate the public AbortSignal API to the native two-phase cancellation
|
|
418
|
+
* operation. Callers receive one ordinary Promise and use the same
|
|
419
|
+
* AbortController as fetch and the streaming APIs.
|
|
420
|
+
*/
|
|
421
|
+
async function runChatSessionCall(
|
|
422
|
+
startCall: () => Promise<NativeChatSessionCall>,
|
|
423
|
+
signal: AbortSignal,
|
|
424
|
+
): Promise<ChatResult> {
|
|
425
|
+
if (signal.aborted) throw new Error("chat session cancelled");
|
|
426
|
+
const call = await startCall();
|
|
427
|
+
let cancelled = false;
|
|
428
|
+
const cancelOnce = (): void => {
|
|
429
|
+
if (cancelled) return;
|
|
430
|
+
cancelled = true;
|
|
431
|
+
call.cancel();
|
|
432
|
+
};
|
|
433
|
+
signal.addEventListener("abort", cancelOnce, { once: true });
|
|
434
|
+
if (signal.aborted) cancelOnce();
|
|
435
|
+
try {
|
|
436
|
+
return await call.result();
|
|
437
|
+
} finally {
|
|
438
|
+
signal.removeEventListener("abort", cancelOnce);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// -------------------------------------------------------------------
|
|
443
|
+
// Generic streaming-model factory
|
|
444
|
+
// -------------------------------------------------------------------
|
|
445
|
+
//
|
|
446
|
+
// Every generative family (Qwen3, Qwen3.5 dense / MoE, LFM2, Gemma4,
|
|
447
|
+
// and the QianfanOCR VLM in `@mlx-node/vlm`) wraps its native class
|
|
448
|
+
// identically: capture the three callback-based session-streaming
|
|
449
|
+
// methods, re-expose them as `AsyncGenerator<ChatStreamEvent>`, set the
|
|
450
|
+
// subclass prototype in `static load`, and (for path-recording families)
|
|
451
|
+
// add `applyChatTemplate`. `makeStreamingModel` builds that subclass
|
|
452
|
+
// once so each family becomes a one-line `extends` declaration.
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The three callback-based session-streaming methods every native chat
|
|
456
|
+
* class carries on its prototype (and, structurally, on its instances).
|
|
457
|
+
* Used both as the native `prototype` shape and as the constructed
|
|
458
|
+
* instance type so `InstanceType<NativeStreamingCtor>` resolves to the
|
|
459
|
+
* full native instance surface (`generate`, `saveModel`,
|
|
460
|
+
* `numParameters`, `hasMtpWeights`, …) — see {@link NativeStreamingCtor}.
|
|
461
|
+
*
|
|
462
|
+
* @internal Native callback streaming surface consumed by
|
|
463
|
+
* {@link makeStreamingModel}.
|
|
464
|
+
*/
|
|
465
|
+
export interface NativeStreamingInstance {
|
|
466
|
+
chatStreamSessionStart: (...args: never[]) => Promise<ChatStreamHandle>;
|
|
467
|
+
chatStreamSessionContinue: (...args: never[]) => Promise<ChatStreamHandle>;
|
|
468
|
+
chatStreamSessionContinueTool: (
|
|
469
|
+
...args: never[]
|
|
470
|
+
) => Promise<ChatStreamHandle>;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Minimal structural shape of a native chat model constructor that the
|
|
475
|
+
* factory needs: a real `new (...)` signature (so `InstanceType<C>`
|
|
476
|
+
* resolves to the native instance surface and the factory return type
|
|
477
|
+
* can preserve `generate`/`saveModel`/`numParameters`/… on the public
|
|
478
|
+
* subclass), a `static load(path)`, and the three callback-based
|
|
479
|
+
* session-streaming methods on its prototype. The native NAPI classes
|
|
480
|
+
* (`Qwen35ModelNative` etc.) all satisfy this — the concrete generic
|
|
481
|
+
* `C` passed at each call site carries the full per-family instance
|
|
482
|
+
* type, which `InstanceType<C>` recovers.
|
|
483
|
+
*/
|
|
484
|
+
interface NativeStreamingCtor {
|
|
485
|
+
// A real constructor signature so `InstanceType<C>` resolves to the
|
|
486
|
+
// concrete native instance type at each call site. The native classes
|
|
487
|
+
// are NAPI-constructed (no public `new`), but structurally they satisfy
|
|
488
|
+
// this and the factory never actually invokes `new` on them.
|
|
489
|
+
new (...args: never[]): NativeStreamingInstance;
|
|
490
|
+
// The native classes resolve `load` to their own concrete instance
|
|
491
|
+
// type; the factory only needs it to be an object, and the public
|
|
492
|
+
// subclass return type re-narrows via `InstanceType<C>` + the
|
|
493
|
+
// `SessionCapableModel` streaming overrides.
|
|
494
|
+
load(modelPath: string): Promise<object>;
|
|
495
|
+
prototype: NativeStreamingInstance;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Tuning knobs for {@link makeStreamingModel}. */
|
|
499
|
+
interface StreamingModelOptions {
|
|
500
|
+
/**
|
|
501
|
+
* When `true`, `static load` records the on-disk model path so the
|
|
502
|
+
* generated subclass can serve `applyChatTemplate` from a lazily
|
|
503
|
+
* constructed tokenizer (see {@link applyChatTemplateFromModelPath}).
|
|
504
|
+
* When `false` (QianfanOCR) the path is not recorded and
|
|
505
|
+
* `applyChatTemplate` is omitted.
|
|
506
|
+
*/
|
|
507
|
+
recordModelPath: boolean;
|
|
508
|
+
/**
|
|
509
|
+
* Whether to attach an `applyChatTemplate` method. Defaults to
|
|
510
|
+
* `recordModelPath` because the method can only work when a path was
|
|
511
|
+
* recorded. Qwen3 (first-gen) records its path but keeps its native
|
|
512
|
+
* tokenizer-backed implementation; pass `applyTemplate: false` to suppress
|
|
513
|
+
* only the factory's path-backed replacement.
|
|
514
|
+
*/
|
|
515
|
+
applyTemplate?: boolean;
|
|
516
|
+
/**
|
|
517
|
+
* Model-specific ordering for structured multimodal content parts. The
|
|
518
|
+
* tokenizer applies this policy after sanitization while the checkpoint
|
|
519
|
+
* Jinja template continues to own all role and wire-format tokens.
|
|
520
|
+
*/
|
|
521
|
+
templateContentPolicy?: TemplateContentPolicy;
|
|
522
|
+
/**
|
|
523
|
+
* Preserve the native raw assistant bytes in session history. LFM2's
|
|
524
|
+
* checkpoint template consumes reasoning inside `message.content` and does
|
|
525
|
+
* not read the structured `reasoning_content` field used by Qwen/Gemma.
|
|
526
|
+
*/
|
|
527
|
+
replayAssistantRawText?: boolean;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Shared base type produced by the factory: a `SessionCapableModel`
|
|
532
|
+
* whose static surface still exposes `load`. Concrete families extend
|
|
533
|
+
* the returned class with an empty body so they inherit everything and
|
|
534
|
+
* pick up the correct `.name` (and working `instanceof`) for free.
|
|
535
|
+
*/
|
|
536
|
+
export type StreamingModel = SessionCapableModel;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* The effective `applyTemplate` flag resolved from the options literal:
|
|
540
|
+
* an explicit `applyTemplate` wins, otherwise it defaults to `recordModelPath`
|
|
541
|
+
* — mirroring the runtime `opts.applyTemplate ?? recordPath`. Requires the
|
|
542
|
+
* options to be inferred as a literal (the `const` type parameter below), so
|
|
543
|
+
* `{ recordModelPath: true }` yields `true`, not `boolean`.
|
|
544
|
+
*/
|
|
545
|
+
type ResolvedApplyTemplate<O extends StreamingModelOptions> = O extends {
|
|
546
|
+
applyTemplate: boolean;
|
|
547
|
+
}
|
|
548
|
+
? O["applyTemplate"]
|
|
549
|
+
: O["recordModelPath"];
|
|
550
|
+
|
|
551
|
+
/** @internal Method names whose callback ABI is replaced by the generator wrapper. */
|
|
552
|
+
export type NativeStreamingMethod = keyof NativeStreamingInstance;
|
|
553
|
+
|
|
554
|
+
type NativeSessionReplacementMethod =
|
|
555
|
+
| "chatSessionStart"
|
|
556
|
+
| "chatSessionContinue"
|
|
557
|
+
| "chatSessionContinueTool"
|
|
558
|
+
| "beginChatSessionStart"
|
|
559
|
+
| "beginChatSessionContinue"
|
|
560
|
+
| "beginChatSessionContinueTool";
|
|
561
|
+
|
|
562
|
+
type StreamingReplacementMethod<O extends StreamingModelOptions> =
|
|
563
|
+
| NativeStreamingMethod
|
|
564
|
+
| NativeSessionReplacementMethod
|
|
565
|
+
| (ResolvedApplyTemplate<O> extends true ? "applyChatTemplate" : never);
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Instance surface of a generated streaming wrapper. Only methods replaced at
|
|
569
|
+
* runtime are removed from the native instance: the three callback streaming
|
|
570
|
+
* methods, the internal operation methods, plus `applyChatTemplate` when the
|
|
571
|
+
* wrapper installs its path-backed implementation. Intersecting the remaining
|
|
572
|
+
* native surface with
|
|
573
|
+
* `SessionCapableModel` preserves required native capabilities such as
|
|
574
|
+
* `hasBlockPagedCache()` while exposing the generator streaming signatures.
|
|
575
|
+
*
|
|
576
|
+
* When `applyTemplate` resolves false, an existing native implementation stays
|
|
577
|
+
* intact (notably Qwen3's required tokenizer-backed method), while models that
|
|
578
|
+
* never had one (QianfanOCR) retain the optional structural contract.
|
|
579
|
+
*
|
|
580
|
+
* @internal Concrete instance type returned by {@link makeStreamingModel}.
|
|
581
|
+
*/
|
|
582
|
+
export type StreamingInstance<
|
|
583
|
+
C extends NativeStreamingCtor,
|
|
584
|
+
O extends StreamingModelOptions,
|
|
585
|
+
> = Omit<InstanceType<C>, StreamingReplacementMethod<O>> &
|
|
586
|
+
SessionCapableModel &
|
|
587
|
+
(ResolvedApplyTemplate<O> extends true
|
|
588
|
+
? Required<Pick<SessionCapableModel, "applyChatTemplate">>
|
|
589
|
+
: object);
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Build the streaming-model subclass for a native chat model class.
|
|
593
|
+
*
|
|
594
|
+
* The returned class:
|
|
595
|
+
* - captures the three native callback-based session-streaming methods
|
|
596
|
+
* from `NativeClass.prototype`,
|
|
597
|
+
* - overrides them as `async *` generators delegating to
|
|
598
|
+
* {@link _runChatStream} with identical argument plumbing (including
|
|
599
|
+
* `config ?? null`, `images`, `isError ?? null`, and the `signal`),
|
|
600
|
+
* - overrides `static load` to re-prototype the native instance onto
|
|
601
|
+
* the concrete subclass (`this`) and optionally record the path,
|
|
602
|
+
* - installs a path-backed `applyChatTemplate` when `opts.applyTemplate`
|
|
603
|
+
* (defaulting to `opts.recordModelPath`).
|
|
604
|
+
*
|
|
605
|
+
* @internal Exported so the VLM wrapper (`@mlx-node/vlm`) builds its
|
|
606
|
+
* `QianfanOCRModel` from the same factory. Not part of the public API.
|
|
607
|
+
*/
|
|
608
|
+
export function makeStreamingModel<
|
|
609
|
+
C extends NativeStreamingCtor,
|
|
610
|
+
const O extends StreamingModelOptions,
|
|
611
|
+
>(
|
|
612
|
+
NativeClass: C,
|
|
613
|
+
opts: O,
|
|
614
|
+
): {
|
|
615
|
+
// Preserve the native instance surface (`generate`, `batchGenerate`,
|
|
616
|
+
// `saveModel`, `numParameters`, required cache-capability getters, …) while
|
|
617
|
+
// replacing only the three callback streaming methods with AsyncGenerators.
|
|
618
|
+
// A path-backed `applyChatTemplate` is also replaced/re-required only on
|
|
619
|
+
// variants that install it (see `StreamingInstance`).
|
|
620
|
+
//
|
|
621
|
+
// `ConstructorParameters<C>` (not `never[]`) keeps each native config
|
|
622
|
+
// constructor — e.g. `new Gemma4Model(config)` / `new QianfanOCRModel(config)`
|
|
623
|
+
// — visible on the generated wrapper for TypeScript consumers. Likewise,
|
|
624
|
+
// `Parameters<C['load']>` keeps each family's native load signature —
|
|
625
|
+
// `[modelPath]` for most, `[modelPath, options?]` for Gemma4 and dense
|
|
626
|
+
// Qwen3.5 external draft models.
|
|
627
|
+
new (...args: ConstructorParameters<C>): StreamingInstance<C, O>;
|
|
628
|
+
load(...args: Parameters<C["load"]>): Promise<StreamingInstance<C, O>>;
|
|
629
|
+
} {
|
|
630
|
+
const recordPath = opts.recordModelPath;
|
|
631
|
+
const applyTemplate = opts.applyTemplate ?? recordPath;
|
|
632
|
+
const templateContentPolicy = opts.templateContentPolicy;
|
|
633
|
+
const replayAssistantRawText = opts.replayAssistantRawText ?? false;
|
|
634
|
+
|
|
635
|
+
// Capture the native callback-based methods before the subclass
|
|
636
|
+
// overrides below shadow them on the prototype.
|
|
637
|
+
const nativeStart = NativeClass.prototype.chatStreamSessionStart;
|
|
638
|
+
const nativeContinue = NativeClass.prototype.chatStreamSessionContinue;
|
|
639
|
+
const nativeContinueTool =
|
|
640
|
+
NativeClass.prototype.chatStreamSessionContinueTool;
|
|
641
|
+
const nativeChat = NativeClass.prototype as NativeStreamingInstance &
|
|
642
|
+
NativeChatSessionOperations;
|
|
643
|
+
|
|
644
|
+
// `NativeClass` is structurally a constructor; cast to a concrete
|
|
645
|
+
// constructor type so `class extends` accepts it. Runtime behavior is
|
|
646
|
+
// unchanged — we extend the real native class.
|
|
647
|
+
const Base = NativeClass as unknown as new (
|
|
648
|
+
...args: never[]
|
|
649
|
+
) => SessionCapableModel;
|
|
650
|
+
|
|
651
|
+
class StreamingModelImpl extends Base {
|
|
652
|
+
supportsReplayReasoningCapture(): boolean {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
replaysAssistantRawText(): boolean {
|
|
657
|
+
return replayAssistantRawText;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
static async load(
|
|
661
|
+
modelPath: string,
|
|
662
|
+
...rest: unknown[]
|
|
663
|
+
): Promise<StreamingModel> {
|
|
664
|
+
// Forward any trailing family-specific load options verbatim (e.g.
|
|
665
|
+
// Gemma4/Qwen3.5 `draftModelPath`); families whose
|
|
666
|
+
// native `load` takes only the path receive no extras. The public
|
|
667
|
+
// signature is re-narrowed per family via `Parameters<C['load']>` in
|
|
668
|
+
// the factory return type below.
|
|
669
|
+
const instance = await (
|
|
670
|
+
NativeClass.load as (...args: unknown[]) => Promise<object>
|
|
671
|
+
)(modelPath, ...rest);
|
|
672
|
+
// Use `this.prototype` (not `StreamingModelImpl.prototype`) so the
|
|
673
|
+
// concrete subclass declared per family supplies the prototype and
|
|
674
|
+
// `instanceof ConcreteSubclass` holds.
|
|
675
|
+
Object.setPrototypeOf(instance, this.prototype);
|
|
676
|
+
if (recordPath) {
|
|
677
|
+
const resolvedAssetsPath = (
|
|
678
|
+
instance as { modelAssetsPath?: () => string }
|
|
679
|
+
).modelAssetsPath?.();
|
|
680
|
+
modelPathsForTokenizers.set(instance, resolvedAssetsPath ?? modelPath);
|
|
681
|
+
}
|
|
682
|
+
return instance as unknown as StreamingModel;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async chatSessionStart(
|
|
686
|
+
messages: ChatMessage[],
|
|
687
|
+
config?: ChatConfig | null,
|
|
688
|
+
signal?: AbortSignal,
|
|
689
|
+
): Promise<ChatResult> {
|
|
690
|
+
if (signal == null) {
|
|
691
|
+
if (nativeChat.chatSessionStart == null) {
|
|
692
|
+
throw new Error("Native model does not implement chatSessionStart");
|
|
693
|
+
}
|
|
694
|
+
return await nativeChat.chatSessionStart.call(this, messages, config);
|
|
695
|
+
}
|
|
696
|
+
if (nativeChat.beginChatSessionStart == null) {
|
|
697
|
+
throw new Error("Native model does not implement beginChatSessionStart");
|
|
698
|
+
}
|
|
699
|
+
return await runChatSessionCall(
|
|
700
|
+
() => nativeChat.beginChatSessionStart!.call(this, messages, config),
|
|
701
|
+
signal,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async chatSessionContinue(
|
|
706
|
+
messages: ChatMessage[],
|
|
707
|
+
config?: ChatConfig | null,
|
|
708
|
+
signal?: AbortSignal,
|
|
709
|
+
): Promise<ChatResult> {
|
|
710
|
+
if (signal == null) {
|
|
711
|
+
if (nativeChat.chatSessionContinue == null) {
|
|
712
|
+
throw new Error("Native model does not implement chatSessionContinue");
|
|
713
|
+
}
|
|
714
|
+
return await nativeChat.chatSessionContinue.call(this, messages, config);
|
|
715
|
+
}
|
|
716
|
+
if (nativeChat.beginChatSessionContinue == null) {
|
|
717
|
+
throw new Error("Native model does not implement beginChatSessionContinue");
|
|
718
|
+
}
|
|
719
|
+
return await runChatSessionCall(
|
|
720
|
+
() => nativeChat.beginChatSessionContinue!.call(this, messages, config),
|
|
721
|
+
signal,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async chatSessionContinueTool(
|
|
726
|
+
messages: ChatMessage[],
|
|
727
|
+
config?: ChatConfig | null,
|
|
728
|
+
signal?: AbortSignal,
|
|
729
|
+
): Promise<ChatResult> {
|
|
730
|
+
if (signal == null) {
|
|
731
|
+
if (nativeChat.chatSessionContinueTool == null) {
|
|
732
|
+
throw new Error("Native model does not implement chatSessionContinueTool");
|
|
733
|
+
}
|
|
734
|
+
return await nativeChat.chatSessionContinueTool.call(this, messages, config);
|
|
735
|
+
}
|
|
736
|
+
if (nativeChat.beginChatSessionContinueTool == null) {
|
|
737
|
+
throw new Error("Native model does not implement beginChatSessionContinueTool");
|
|
738
|
+
}
|
|
739
|
+
return await runChatSessionCall(
|
|
740
|
+
() => nativeChat.beginChatSessionContinueTool!.call(this, messages, config),
|
|
741
|
+
signal,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// The native methods are callback-based, but `Base` is typed as a
|
|
746
|
+
// `SessionCapableModel` constructor (whose streaming methods already
|
|
747
|
+
// return `AsyncGenerator<ChatStreamEvent>`), so these overrides are
|
|
748
|
+
// type-compatible and need no `@ts-expect-error` suppression. The
|
|
749
|
+
// callback bridging happens at runtime via the captured natives.
|
|
750
|
+
async *chatStreamSessionStart(
|
|
751
|
+
messages: ChatMessage[],
|
|
752
|
+
config?: ChatConfig | null,
|
|
753
|
+
signal?: AbortSignal,
|
|
754
|
+
): AsyncGenerator<ChatStreamEvent> {
|
|
755
|
+
yield* _runChatStream(
|
|
756
|
+
(callback) =>
|
|
757
|
+
nativeStart.call(
|
|
758
|
+
this,
|
|
759
|
+
messages as never,
|
|
760
|
+
(config ?? null) as never,
|
|
761
|
+
callback as never,
|
|
762
|
+
),
|
|
763
|
+
signal,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async *chatStreamSessionContinue(
|
|
768
|
+
messages: ChatMessage[],
|
|
769
|
+
config?: ChatConfig | null,
|
|
770
|
+
signal?: AbortSignal,
|
|
771
|
+
): AsyncGenerator<ChatStreamEvent> {
|
|
772
|
+
yield* _runChatStream(
|
|
773
|
+
(callback) =>
|
|
774
|
+
nativeContinue.call(
|
|
775
|
+
this,
|
|
776
|
+
messages as never,
|
|
777
|
+
(config ?? null) as never,
|
|
778
|
+
callback as never,
|
|
779
|
+
),
|
|
780
|
+
signal,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async *chatStreamSessionContinueTool(
|
|
785
|
+
messages: ChatMessage[],
|
|
786
|
+
config?: ChatConfig | null,
|
|
787
|
+
signal?: AbortSignal,
|
|
788
|
+
): AsyncGenerator<ChatStreamEvent> {
|
|
789
|
+
yield* _runChatStream(
|
|
790
|
+
(callback) =>
|
|
791
|
+
nativeContinueTool.call(
|
|
792
|
+
this,
|
|
793
|
+
messages as never,
|
|
794
|
+
(config ?? null) as never,
|
|
795
|
+
callback as never,
|
|
796
|
+
),
|
|
797
|
+
signal,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (applyTemplate) {
|
|
803
|
+
Object.defineProperty(StreamingModelImpl.prototype, "applyChatTemplate", {
|
|
804
|
+
configurable: true,
|
|
805
|
+
writable: true,
|
|
806
|
+
value(
|
|
807
|
+
this: object,
|
|
808
|
+
messages: ChatMessage[],
|
|
809
|
+
addGenerationPrompt?: boolean | null,
|
|
810
|
+
tools?: ToolDefinition[] | null,
|
|
811
|
+
enableThinking?: boolean | null,
|
|
812
|
+
reasoningEffort?: string | null,
|
|
813
|
+
): Promise<Uint32Array> {
|
|
814
|
+
return applyChatTemplateFromModelPath(
|
|
815
|
+
this,
|
|
816
|
+
messages,
|
|
817
|
+
addGenerationPrompt,
|
|
818
|
+
tools,
|
|
819
|
+
enableThinking,
|
|
820
|
+
templateContentPolicy,
|
|
821
|
+
reasoningEffort,
|
|
822
|
+
);
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return StreamingModelImpl as unknown as {
|
|
828
|
+
new (...args: ConstructorParameters<C>): StreamingInstance<C, O>;
|
|
829
|
+
load(...args: Parameters<C["load"]>): Promise<StreamingInstance<C, O>>;
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Qwen3.5 dense model with AsyncGenerator-based session streaming.
|
|
835
|
+
*
|
|
836
|
+
* The empty `extends` inherits the factory's streaming overrides,
|
|
837
|
+
* `static load`, and `applyChatTemplate`, and supplies the concrete
|
|
838
|
+
* `.name === 'Qwen35Model'` and a working `instanceof`. Records its
|
|
839
|
+
* model path so `applyChatTemplate` can serve a lazily built tokenizer.
|
|
840
|
+
*/
|
|
841
|
+
export class Qwen35Model extends makeStreamingModel(Qwen35ModelNative, {
|
|
842
|
+
recordModelPath: true,
|
|
843
|
+
}) {}
|
|
844
|
+
|
|
845
|
+
/** Qwen3.5 MoE model — see {@link Qwen35Model} for the wrapper shape. */
|
|
846
|
+
export class Qwen35MoeModel extends makeStreamingModel(Qwen35MoeModelNative, {
|
|
847
|
+
recordModelPath: true,
|
|
848
|
+
}) {}
|
|
849
|
+
|
|
850
|
+
/** LFM2 model (text-only) — see {@link Qwen35Model} for the wrapper shape. */
|
|
851
|
+
export class Lfm2Model extends makeStreamingModel(Lfm2ModelNative, {
|
|
852
|
+
recordModelPath: true,
|
|
853
|
+
replayAssistantRawText: true,
|
|
854
|
+
}) {}
|
|
855
|
+
|
|
856
|
+
/** Nemotron 3.5 Lightning (text-only) — see {@link Qwen35Model} for the wrapper shape. */
|
|
857
|
+
export class NemotronHModel extends makeStreamingModel(NemotronHModelNative, {
|
|
858
|
+
recordModelPath: true,
|
|
859
|
+
}) {}
|
|
860
|
+
|
|
861
|
+
/** Gemma4 model (text-only) — see {@link Qwen35Model} for the wrapper shape. */
|
|
862
|
+
export class Gemma4Model extends makeStreamingModel(Gemma4ModelNative, {
|
|
863
|
+
recordModelPath: true,
|
|
864
|
+
}) {}
|
|
865
|
+
|
|
866
|
+
/** Muse-Glimmer text model with embedded DFlash speculative decoding. */
|
|
867
|
+
export class MuseGlimmerModel extends makeStreamingModel(MuseGlimmerModelNative, {
|
|
868
|
+
recordModelPath: true,
|
|
869
|
+
}) {}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Qwen3 (first-gen, text-only) model.
|
|
873
|
+
*
|
|
874
|
+
* Records its model path (so prototype-set + path-recording match the
|
|
875
|
+
* other families) but does not install the factory's path-backed
|
|
876
|
+
* `applyChatTemplate`; it retains the native tokenizer-backed method.
|
|
877
|
+
*/
|
|
878
|
+
export class Qwen3Model extends makeStreamingModel(Qwen3ModelNative, {
|
|
879
|
+
recordModelPath: true,
|
|
880
|
+
applyTemplate: false,
|
|
881
|
+
}) {}
|
|
882
|
+
|
|
883
|
+
// -------------------------------------------------------------------
|
|
884
|
+
// Compile-time conformance check
|
|
885
|
+
// -------------------------------------------------------------------
|
|
886
|
+
//
|
|
887
|
+
// Ensures each family class structurally satisfies
|
|
888
|
+
// `SessionCapableModel` so `ChatSession<XxxModel>` type-checks in
|
|
889
|
+
// downstream code. Compile-only — the `null as unknown as T`
|
|
890
|
+
// placeholder never runs. If a factory override signature drifts away
|
|
891
|
+
// from the interface, this block fails to compile.
|
|
892
|
+
function _assertSessionCapable(): void {
|
|
893
|
+
const _qwen35: SessionCapableModel = null as unknown as Qwen35Model;
|
|
894
|
+
const _moe: SessionCapableModel = null as unknown as Qwen35MoeModel;
|
|
895
|
+
const _lfm2: SessionCapableModel = null as unknown as Lfm2Model;
|
|
896
|
+
const _gemma4: SessionCapableModel = null as unknown as Gemma4Model;
|
|
897
|
+
const _museGlimmer: SessionCapableModel = null as unknown as MuseGlimmerModel;
|
|
898
|
+
const _qwen3: SessionCapableModel = null as unknown as Qwen3Model;
|
|
899
|
+
const _nemotronH: SessionCapableModel = null as unknown as NemotronHModel;
|
|
900
|
+
void _qwen35;
|
|
901
|
+
void _moe;
|
|
902
|
+
void _lfm2;
|
|
903
|
+
void _gemma4;
|
|
904
|
+
void _museGlimmer;
|
|
905
|
+
void _qwen3;
|
|
906
|
+
void _nemotronH;
|
|
907
|
+
}
|
|
908
|
+
void _assertSessionCapable;
|
|
909
|
+
|
|
910
|
+
type ExpandedPromptPlanner = Required<
|
|
911
|
+
Pick<SessionCapableModel, "expandedPromptTokenCount">
|
|
912
|
+
>;
|
|
913
|
+
|
|
914
|
+
/** Compile-time guard that both Qwen3.5 native classes and wrappers retain the exact media planner. */
|
|
915
|
+
function _assertExpandedPromptPlannerSurfaces(): void {
|
|
916
|
+
const _nativeDense: ExpandedPromptPlanner = null as unknown as InstanceType<
|
|
917
|
+
typeof Qwen35ModelNative
|
|
918
|
+
>;
|
|
919
|
+
const _nativeMoe: ExpandedPromptPlanner = null as unknown as InstanceType<
|
|
920
|
+
typeof Qwen35MoeModelNative
|
|
921
|
+
>;
|
|
922
|
+
const _wrappedDense: ExpandedPromptPlanner = null as unknown as Qwen35Model;
|
|
923
|
+
const _wrappedMoe: ExpandedPromptPlanner = null as unknown as Qwen35MoeModel;
|
|
924
|
+
void _nativeDense;
|
|
925
|
+
void _nativeMoe;
|
|
926
|
+
void _wrappedDense;
|
|
927
|
+
void _wrappedMoe;
|
|
928
|
+
}
|
|
929
|
+
void _assertExpandedPromptPlannerSurfaces;
|
|
930
|
+
|
|
931
|
+
type PreservedNativeSurface<C extends NativeStreamingCtor> = Omit<
|
|
932
|
+
InstanceType<C>,
|
|
933
|
+
NativeStreamingMethod | NativeSessionReplacementMethod
|
|
934
|
+
>;
|
|
935
|
+
|
|
936
|
+
/** Compile-time guard for every native member the factory does not replace. */
|
|
937
|
+
function _assertPreservedNativeSurfaces(): void {
|
|
938
|
+
const _qwen3: PreservedNativeSurface<typeof Qwen3ModelNative> =
|
|
939
|
+
null as unknown as Qwen3Model;
|
|
940
|
+
const _qwen35: PreservedNativeSurface<typeof Qwen35ModelNative> =
|
|
941
|
+
null as unknown as Qwen35Model;
|
|
942
|
+
const _moe: PreservedNativeSurface<typeof Qwen35MoeModelNative> =
|
|
943
|
+
null as unknown as Qwen35MoeModel;
|
|
944
|
+
const _lfm2: PreservedNativeSurface<typeof Lfm2ModelNative> =
|
|
945
|
+
null as unknown as Lfm2Model;
|
|
946
|
+
const _gemma4: PreservedNativeSurface<typeof Gemma4ModelNative> =
|
|
947
|
+
null as unknown as Gemma4Model;
|
|
948
|
+
const _museGlimmer: PreservedNativeSurface<typeof MuseGlimmerModelNative> =
|
|
949
|
+
null as unknown as MuseGlimmerModel;
|
|
950
|
+
const _nemotronH: PreservedNativeSurface<typeof NemotronHModelNative> =
|
|
951
|
+
null as unknown as NemotronHModel;
|
|
952
|
+
void _qwen3;
|
|
953
|
+
void _qwen35;
|
|
954
|
+
void _moe;
|
|
955
|
+
void _lfm2;
|
|
956
|
+
void _gemma4;
|
|
957
|
+
void _museGlimmer;
|
|
958
|
+
void _nemotronH;
|
|
959
|
+
}
|
|
960
|
+
void _assertPreservedNativeSurfaces;
|