@mlx-node/lm 0.0.5 → 0.0.7

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,19 +1,86 @@
1
- import { Qwen35Model as Qwen35ModelNative, Qwen35MoeModel as Qwen35MoeModelNative } from '@mlx-node/core';
2
- // Save references to the native callback-based methods before we override them
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
3
12
  // oxlint-disable-next-line @typescript-eslint/unbound-method
4
- const _nativeDenseChatStream = Qwen35ModelNative.prototype.chatStream;
13
+ const _nativeDenseChatStreamSessionStart = Qwen35ModelNative.prototype.chatStreamSessionStart;
5
14
  // oxlint-disable-next-line @typescript-eslint/unbound-method
6
- const _nativeMoeChatStream = Qwen35MoeModelNative.prototype.chatStream;
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;
7
46
  /**
8
- * Shared AsyncGenerator implementation that wraps a native callback-based
9
- * chatStream into a `for await...of`-compatible stream.
47
+ * Shared AsyncGenerator adapter for callback-based native streaming methods.
48
+ *
49
+ * Takes a `startCall` closure that, given the JS-side callback, dispatches
50
+ * the underlying native stream (whatever method signature that is — the
51
+ * closure captures `messages` / `config` / `userMessage` etc) and resolves
52
+ * with a `ChatStreamHandle`. The generator pumps the resulting chunk queue,
53
+ * transforms each chunk into a `ChatStreamEvent`, and calls `handle.cancel()`
54
+ * in a `finally` block so early termination (user `break`, exception) still
55
+ * cleans up native state.
56
+ *
57
+ * ## Signal-driven fast-abort
58
+ *
59
+ * When an optional `AbortSignal` is supplied and fires, the adapter:
60
+ * 1. Calls `handle.cancel()` immediately so the native decode stops
61
+ * on the next safepoint rather than running to completion.
62
+ * 2. Wakes the pending `waitForItem()` await by pushing a synthetic
63
+ * "aborted" marker into the queue and calling `notify()`. Without
64
+ * this wake-up the generator would stay parked on the `await`
65
+ * until the next native chunk arrived, which on a fast-abort
66
+ * path (client disconnect before first token) never happens.
67
+ * 3. The generator sees the marker, breaks out of its loop, and the
68
+ * finally block runs `cancelOnce()` — which is a no-op because
69
+ * `triggerAbort` already flipped the `cancelled` flag. Some
70
+ * backends throw on double-cancel, so routing every cancel site
71
+ * through `cancelOnce` keeps abort behavior deterministic.
10
72
  *
11
- * Cancellation is automatic via the generator's `finally` block.
73
+ * The finally block is also the landing site for the consumer calling
74
+ * `.return()` on the outer generator — the existing `yield` cleanup
75
+ * covers that case. Signal-driven abort covers the window where the
76
+ * consumer cannot reach `.return()` because they are blocked waiting
77
+ * for the very `yield` that `waitForItem()` is gating.
78
+ *
79
+ * @internal Exported so the VLM wrapper (`@mlx-node/vlm`) can reuse the
80
+ * exact same bridge without duplicating the plumbing. Not part of the
81
+ * public API — may change without notice.
12
82
  */
13
- /** @internal Exported for testing only. */
14
- export async function* _createChatStream(
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
- nativeMethod, self, messages, config) {
83
+ export async function* _runChatStream(startCall, signal) {
17
84
  const queue = [];
18
85
  let resolve = null;
19
86
  const waitForItem = () => queue.length > 0
@@ -32,12 +99,74 @@ nativeMethod, self, messages, config) {
32
99
  queue.push(err ? { error: err } : { chunk });
33
100
  notify();
34
101
  };
35
- const handle = await nativeMethod.call(self, messages, config ?? null, callback);
102
+ const handle = await startCall(callback);
103
+ // Guard against double-cancel. Some native backends throw on a
104
+ // second `cancel()`; we route every cancel site through this
105
+ // helper so the abort path (via `triggerAbort`) and the unwind
106
+ // path (via the `finally` block) don't cancel twice and so any
107
+ // backend that does throw is swallowed rather than escaping as
108
+ // an error out of an otherwise-clean early termination.
109
+ let cancelled = false;
110
+ const cancelOnce = () => {
111
+ if (cancelled)
112
+ return;
113
+ cancelled = true;
114
+ try {
115
+ handle.cancel();
116
+ }
117
+ catch {
118
+ // Native backend threw on cancel — nothing actionable here.
119
+ // Swallow so aborted streams still surface as a clean early
120
+ // termination via the synthetic `aborted` marker rather than
121
+ // as an unexpected error out of the generator.
122
+ }
123
+ };
124
+ // Signal-driven fast-abort. If the signal is already aborted at
125
+ // attach time we still arm the listener so the synchronous abort
126
+ // dispatch path runs below (calling `handle.cancel()` after the
127
+ // handle is in hand, not before — there is nothing to cancel
128
+ // pre-start). For an already-fired signal Node delivers the
129
+ // `'abort'` event on the next microtask.
130
+ let onAbort = null;
131
+ if (signal != null) {
132
+ const triggerAbort = () => {
133
+ // Cancel the native side first so any work-in-flight winds
134
+ // down ASAP. `cancelOnce` is idempotent — the finally block
135
+ // will invoke it again after the generator unwinds, but the
136
+ // second call becomes a no-op.
137
+ cancelOnce();
138
+ // Push a synthetic abort marker so the consumer-visible
139
+ // generator breaks out of its loop at the next iteration
140
+ // rather than waiting for a native chunk that will never
141
+ // arrive (e.g. client disconnect before first token).
142
+ queue.push({ aborted: true });
143
+ notify();
144
+ };
145
+ if (signal.aborted) {
146
+ // Signal already fired before we could attach — dispatch
147
+ // synchronously so the waitForItem below resolves immediately.
148
+ triggerAbort();
149
+ }
150
+ else {
151
+ onAbort = triggerAbort;
152
+ signal.addEventListener('abort', onAbort, { once: true });
153
+ }
154
+ }
36
155
  try {
37
- while (true) {
156
+ loop: while (true) {
38
157
  await waitForItem();
39
158
  while (queue.length > 0) {
40
159
  const item = queue.shift();
160
+ if (item.aborted) {
161
+ // Consumer asked us to stop before the next chunk landed.
162
+ // Break out so the finally block runs `handle.cancel()`
163
+ // (idempotent — `triggerAbort` already called it) and the
164
+ // consumer-side `for await` unblocks cleanly. We do NOT
165
+ // throw an AbortError: callers (e.g. server endpoints that
166
+ // flag client disconnect) have already decided this is not
167
+ // an error from their perspective, just an early stop.
168
+ break loop;
169
+ }
41
170
  if (item.error)
42
171
  throw item.error;
43
172
  const chunk = item.chunk;
@@ -49,29 +178,38 @@ nativeMethod, self, messages, config) {
49
178
  toolCalls: chunk.toolCalls ?? [],
50
179
  thinking: chunk.thinking ?? null,
51
180
  numTokens: chunk.numTokens,
181
+ promptTokens: chunk.promptTokens ?? 0,
182
+ reasoningTokens: chunk.reasoningTokens ?? 0,
52
183
  rawText: chunk.rawText,
53
184
  performance: chunk.performance ?? undefined,
54
185
  };
55
186
  return;
56
187
  }
57
- yield { text: chunk.text, done: false };
188
+ yield { text: chunk.text, done: false, isReasoning: chunk.isReasoning ?? undefined };
58
189
  }
59
190
  }
60
191
  }
61
192
  finally {
62
- handle.cancel();
193
+ if (signal != null && onAbort != null) {
194
+ try {
195
+ signal.removeEventListener('abort', onAbort);
196
+ }
197
+ catch {
198
+ // removeEventListener shouldn't throw, but stay defensive —
199
+ // a misbehaving signal must not leak out of the finally.
200
+ }
201
+ }
202
+ cancelOnce();
63
203
  }
64
204
  }
65
205
  /**
66
- * Qwen3.5 dense model with AsyncGenerator-based `chatStream()`.
206
+ * Qwen3.5 dense model with AsyncGenerator-based session streaming.
67
207
  *
68
- * @example
69
- * ```typescript
70
- * const model = await Qwen35Model.load('./models/qwen3.5-3b');
71
- * for await (const event of model.chatStream(messages)) {
72
- * if (!event.done) process.stdout.write(event.text);
73
- * }
74
- * ```
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>`.
75
213
  */
76
214
  export class Qwen35Model extends Qwen35ModelNative {
77
215
  static async load(modelPath) {
@@ -79,21 +217,62 @@ export class Qwen35Model extends Qwen35ModelNative {
79
217
  Object.setPrototypeOf(instance, Qwen35Model.prototype);
80
218
  return instance;
81
219
  }
82
- // @ts-expect-error — override callback-based chatStream with AsyncGenerator
83
- async *chatStream(messages, config) {
84
- yield* _createChatStream(_nativeDenseChatStream, this, messages, config);
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);
85
267
  }
86
268
  }
87
269
  /**
88
- * Qwen3.5 MoE model with AsyncGenerator-based `chatStream()`.
270
+ * Qwen3.5 MoE model wrapper.
89
271
  *
90
- * @example
91
- * ```typescript
92
- * const model = await Qwen35MoeModel.load('./models/qwen3.5-moe');
93
- * for await (const event of model.chatStream(messages)) {
94
- * if (!event.done) process.stdout.write(event.text);
95
- * }
96
- * ```
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`.
97
276
  */
98
277
  export class Qwen35MoeModel extends Qwen35MoeModelNative {
99
278
  static async load(modelPath) {
@@ -101,8 +280,138 @@ export class Qwen35MoeModel extends Qwen35MoeModelNative {
101
280
  Object.setPrototypeOf(instance, Qwen35MoeModel.prototype);
102
281
  return instance;
103
282
  }
104
- // @ts-expect-error override callback-based chatStream with AsyncGenerator
105
- async *chatStream(messages, config) {
106
- yield* _createChatStream(_nativeMoeChatStream, this, messages, config);
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-error — override 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);
292
+ }
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);
297
+ }
298
+ }
299
+ /**
300
+ * LFM2 model wrapper.
301
+ *
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.
308
+ */
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);
107
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
+ }
330
+ }
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
+ }
362
+ }
363
+ /**
364
+ * Qwen3 (legacy) model wrapper.
365
+ *
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.
372
+ */
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
+ }
394
+ }
395
+ // -------------------------------------------------------------------
396
+ // Compile-time conformance check
397
+ // -------------------------------------------------------------------
398
+ //
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.
405
+ function _assertSessionCapable() {
406
+ const _qwen35 = null;
407
+ const _moe = null;
408
+ const _lfm2 = null;
409
+ const _gemma4 = null;
410
+ const _qwen3 = null;
411
+ void _qwen35;
412
+ void _moe;
413
+ void _lfm2;
414
+ void _gemma4;
415
+ void _qwen3;
108
416
  }
417
+ void _assertSessionCapable;
@@ -1,27 +1,54 @@
1
1
  /**
2
- * Tool calling utilities for Qwen3
2
+ * Tool calling utilities
3
3
  *
4
- * Provides types and helpers for working with tool/function calling in the chat() API.
4
+ * Provides types and helpers for working with tool/function calling
5
+ * through the `ChatSession` API. Tools are passed via `ChatConfig.tools`
6
+ * on `session.send()`, and a tool result is fed back through
7
+ * `session.sendToolResult()`.
8
+ *
9
+ * **Single tool call per assistant turn.** Each `sendToolResult(...)`
10
+ * call appends one tool message and immediately re-opens the assistant
11
+ * turn, so the session API only supports assistant turns that emit
12
+ * exactly one tool call. If the model emits multiple tool calls in a
13
+ * single turn, the caller must treat that as an unsupported state:
14
+ * throw, surface an error to the user, or tighten the system prompt /
15
+ * tool spec so the model produces at most one call per turn. Do **not**
16
+ * loop `sendToolResult` across the remaining calls — later results
17
+ * would be interleaved with a new assistant reply from the first, and
18
+ * the conversation state would become inconsistent (especially for
19
+ * stateful tools whose effects must land in order).
5
20
  *
6
21
  * @example
7
22
  * ```typescript
8
- * import { createToolDefinition, formatToolResponse } from '@mlx-node/lm';
23
+ * import { createToolDefinition, loadSession } from '@mlx-node/lm';
9
24
  *
10
25
  * const weatherTool = createToolDefinition(
11
26
  * 'get_weather',
12
27
  * 'Get weather for a location',
13
28
  * { location: { type: 'string', description: 'City name' } },
14
- * ['location']
29
+ * ['location'],
15
30
  * );
16
31
  *
17
- * const result = await model.chat(messages, { tools: [weatherTool] });
32
+ * const session = await loadSession('./my-model');
33
+ *
34
+ * const result = await session.send('What is the weather in Tokyo?', {
35
+ * config: { tools: [weatherTool] },
36
+ * });
18
37
  *
19
- * for (const call of result.toolCalls) {
20
- * if (call.status === 'ok') {
21
- * const toolResult = await executeMyTool(call.name, call.arguments);
22
- * // Continue conversation with tool result
23
- * messages.push({ role: 'user', content: formatToolResponse(toolResult) });
24
- * }
38
+ * const okCalls = result.toolCalls.filter((c) => c.status === 'ok');
39
+ * if (okCalls.length > 1) {
40
+ * throw new Error(
41
+ * `ChatSession only supports one tool call per assistant turn; ` +
42
+ * `model emitted ${okCalls.length}. Tighten the prompt or tool spec.`,
43
+ * );
44
+ * }
45
+ * const call = okCalls[0];
46
+ * if (call) {
47
+ * const toolOutput = await executeMyTool(call.name, call.arguments);
48
+ * const followUp = await session.sendToolResult(call.id, JSON.stringify(toolOutput), {
49
+ * config: { tools: [weatherTool] },
50
+ * });
51
+ * console.log(followUp.text);
25
52
  * }
26
53
  * ```
27
54
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,cAAc,YAAY,CAAC"}
@@ -1,27 +1,54 @@
1
1
  /**
2
- * Tool calling utilities for Qwen3
2
+ * Tool calling utilities
3
3
  *
4
- * Provides types and helpers for working with tool/function calling in the chat() API.
4
+ * Provides types and helpers for working with tool/function calling
5
+ * through the `ChatSession` API. Tools are passed via `ChatConfig.tools`
6
+ * on `session.send()`, and a tool result is fed back through
7
+ * `session.sendToolResult()`.
8
+ *
9
+ * **Single tool call per assistant turn.** Each `sendToolResult(...)`
10
+ * call appends one tool message and immediately re-opens the assistant
11
+ * turn, so the session API only supports assistant turns that emit
12
+ * exactly one tool call. If the model emits multiple tool calls in a
13
+ * single turn, the caller must treat that as an unsupported state:
14
+ * throw, surface an error to the user, or tighten the system prompt /
15
+ * tool spec so the model produces at most one call per turn. Do **not**
16
+ * loop `sendToolResult` across the remaining calls — later results
17
+ * would be interleaved with a new assistant reply from the first, and
18
+ * the conversation state would become inconsistent (especially for
19
+ * stateful tools whose effects must land in order).
5
20
  *
6
21
  * @example
7
22
  * ```typescript
8
- * import { createToolDefinition, formatToolResponse } from '@mlx-node/lm';
23
+ * import { createToolDefinition, loadSession } from '@mlx-node/lm';
9
24
  *
10
25
  * const weatherTool = createToolDefinition(
11
26
  * 'get_weather',
12
27
  * 'Get weather for a location',
13
28
  * { location: { type: 'string', description: 'City name' } },
14
- * ['location']
29
+ * ['location'],
15
30
  * );
16
31
  *
17
- * const result = await model.chat(messages, { tools: [weatherTool] });
32
+ * const session = await loadSession('./my-model');
33
+ *
34
+ * const result = await session.send('What is the weather in Tokyo?', {
35
+ * config: { tools: [weatherTool] },
36
+ * });
18
37
  *
19
- * for (const call of result.toolCalls) {
20
- * if (call.status === 'ok') {
21
- * const toolResult = await executeMyTool(call.name, call.arguments);
22
- * // Continue conversation with tool result
23
- * messages.push({ role: 'user', content: formatToolResponse(toolResult) });
24
- * }
38
+ * const okCalls = result.toolCalls.filter((c) => c.status === 'ok');
39
+ * if (okCalls.length > 1) {
40
+ * throw new Error(
41
+ * `ChatSession only supports one tool call per assistant turn; ` +
42
+ * `model emitted ${okCalls.length}. Tighten the prompt or tool spec.`,
43
+ * );
44
+ * }
45
+ * const call = okCalls[0];
46
+ * if (call) {
47
+ * const toolOutput = await executeMyTool(call.name, call.arguments);
48
+ * const followUp = await session.sendToolResult(call.id, JSON.stringify(toolOutput), {
49
+ * config: { tools: [weatherTool] },
50
+ * });
51
+ * console.log(followUp.text);
25
52
  * }
26
53
  * ```
27
54
  *
@@ -164,7 +164,8 @@ export interface ApplyChatTemplateOptions {
164
164
  * @param description - Description of what the function does
165
165
  * @param properties - Object defining the function parameters (will be JSON stringified)
166
166
  * @param required - Array of required parameter names
167
- * @returns A properly formatted ToolDefinition ready for use with model.chat()
167
+ * @returns A properly formatted ToolDefinition ready to pass via `ChatConfig.tools`
168
+ * on `ChatSession.send()` / `sendToolResult()`.
168
169
  *
169
170
  * @example
170
171
  * ```typescript
@@ -173,34 +174,13 @@ export interface ApplyChatTemplateOptions {
173
174
  * 'Get weather information for a location',
174
175
  * {
175
176
  * location: { type: 'string', description: 'City name' },
176
- * units: { type: 'string', enum: ['celsius', 'fahrenheit'] }
177
+ * units: { type: 'string', enum: ['celsius', 'fahrenheit'] },
177
178
  * },
178
- * ['location']
179
+ * ['location'],
179
180
  * );
180
181
  *
181
- * const result = await model.chat(messages, { tools: [weatherTool] });
182
+ * const result = await session.send(userPrompt, { config: { tools: [weatherTool] } });
182
183
  * ```
183
184
  */
184
185
  export declare function createToolDefinition(name: string, description?: string, properties?: Record<string, FunctionParameterProperty>, required?: string[]): ToolDefinition;
185
- /**
186
- * Format a tool response for inclusion in a message
187
- *
188
- * Creates a properly formatted tool response string that can be used
189
- * in tool messages when continuing a conversation after a tool call.
190
- *
191
- * @param content - The response content (will be JSON stringified if object)
192
- * @returns Formatted tool response string wrapped in `<tool_response>` tags
193
- *
194
- * @example
195
- * ```typescript
196
- * // After executing a tool call from model.chat()
197
- * const toolResult = await executeMyTool(call.arguments);
198
- * const responseMessage = {
199
- * role: 'user',
200
- * content: formatToolResponse(toolResult)
201
- * };
202
- * const finalResult = await model.chat([...messages, responseMessage]);
203
- * ```
204
- */
205
- export declare function formatToolResponse(content: unknown): string;
206
186
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/tools/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC;AAElC;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,yBAAyB,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;IACvD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,IAAI,EAAE,QAAQ,CAAC;IACf;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,IAAI,EAAE,QAAQ,CAAC;IACf,0BAA0B;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,mBAAmB;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,8DAA8D;IAC9D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,qDAAqD;IACrD,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,EACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,EACtD,QAAQ,CAAC,EAAE,MAAM,EAAE,GAClB,cAAc,CAehB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAK3D"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/tools/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC;AAElC;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,yBAAyB,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;IACvD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,IAAI,EAAE,QAAQ,CAAC;IACf;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,IAAI,EAAE,QAAQ,CAAC;IACf,0BAA0B;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,mBAAmB;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,8DAA8D;IAC9D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,qDAAqD;IACrD,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,EACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,EACtD,QAAQ,CAAC,EAAE,MAAM,EAAE,GAClB,cAAc,CAehB"}