@mlx-node/lm 0.0.6 → 0.0.8

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,64 @@
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
3
- // oxlint-disable-next-line @typescript-eslint/unbound-method
4
- const _nativeDenseChatStream = Qwen35ModelNative.prototype.chatStream;
5
- // oxlint-disable-next-line @typescript-eslint/unbound-method
6
- const _nativeMoeChatStream = Qwen35MoeModelNative.prototype.chatStream;
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) {
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
+ return tokenizer.applyChatTemplate(messages, addGenerationPrompt, tools, enableThinking);
23
+ }
7
24
  /**
8
- * Shared AsyncGenerator implementation that wraps a native callback-based
9
- * chatStream into a `for await...of`-compatible stream.
25
+ * Shared AsyncGenerator adapter for callback-based native streaming methods.
10
26
  *
11
- * Cancellation is automatic via the generator's `finally` block.
27
+ * Takes a `startCall` closure that, given the JS-side callback, dispatches
28
+ * the underlying native stream (whatever method signature that is — the
29
+ * closure captures `messages` / `config` / `userMessage` etc) and resolves
30
+ * with a `ChatStreamHandle`. The generator pumps the resulting chunk queue,
31
+ * transforms each chunk into a `ChatStreamEvent`, and calls `handle.cancel()`
32
+ * in a `finally` block so early termination (user `break`, exception) still
33
+ * cleans up native state.
34
+ *
35
+ * ## Signal-driven fast-abort
36
+ *
37
+ * When an optional `AbortSignal` is supplied and fires, the adapter:
38
+ * 1. Calls `handle.cancel()` immediately so the native decode stops
39
+ * on the next safepoint rather than running to completion.
40
+ * 2. Wakes the pending `waitForItem()` await by pushing a synthetic
41
+ * "aborted" marker into the queue and calling `notify()`. Without
42
+ * this wake-up the generator would stay parked on the `await`
43
+ * until the next native chunk arrived, which on a fast-abort
44
+ * path (client disconnect before first token) never happens.
45
+ * 3. The generator sees the marker, breaks out of its loop, and the
46
+ * finally block runs `cancelOnce()` — which is a no-op because
47
+ * `triggerAbort` already flipped the `cancelled` flag. Some
48
+ * backends throw on double-cancel, so routing every cancel site
49
+ * through `cancelOnce` keeps abort behavior deterministic.
50
+ *
51
+ * The finally block is also the landing site for the consumer calling
52
+ * `.return()` on the outer generator — the existing `yield` cleanup
53
+ * covers that case. Signal-driven abort covers the window where the
54
+ * consumer cannot reach `.return()` because they are blocked waiting
55
+ * for the very `yield` that `waitForItem()` is gating.
56
+ *
57
+ * @internal Exported so the VLM wrapper (`@mlx-node/vlm`) can reuse the
58
+ * exact same bridge without duplicating the plumbing. Not part of the
59
+ * public API — may change without notice.
12
60
  */
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) {
61
+ export async function* _runChatStream(startCall, signal) {
17
62
  const queue = [];
18
63
  let resolve = null;
19
64
  const waitForItem = () => queue.length > 0
@@ -32,77 +77,274 @@ nativeMethod, self, messages, config) {
32
77
  queue.push(err ? { error: err } : { chunk });
33
78
  notify();
34
79
  };
35
- const handle = await nativeMethod.call(self, messages, config ?? null, callback);
80
+ const handle = await startCall(callback);
81
+ // Guard against double-cancel. Some native backends throw on a
82
+ // second `cancel()`; we route every cancel site through this
83
+ // helper so the abort path (via `triggerAbort`) and the unwind
84
+ // path (via the `finally` block) don't cancel twice and so any
85
+ // backend that does throw is swallowed rather than escaping as
86
+ // an error out of an otherwise-clean early termination.
87
+ let cancelled = false;
88
+ const cancelOnce = () => {
89
+ if (cancelled)
90
+ return;
91
+ cancelled = true;
92
+ try {
93
+ handle.cancel();
94
+ }
95
+ catch {
96
+ // Native backend threw on cancel — nothing actionable here.
97
+ // Swallow so aborted streams still surface as a clean early
98
+ // termination via the synthetic `aborted` marker rather than
99
+ // as an unexpected error out of the generator.
100
+ }
101
+ };
102
+ // Signal-driven fast-abort. If the signal is already aborted at
103
+ // attach time we still arm the listener so the synchronous abort
104
+ // dispatch path runs below (calling `handle.cancel()` after the
105
+ // handle is in hand, not before — there is nothing to cancel
106
+ // pre-start). For an already-fired signal Node delivers the
107
+ // `'abort'` event on the next microtask.
108
+ let onAbort = null;
109
+ if (signal != null) {
110
+ const triggerAbort = () => {
111
+ // Cancel the native side first so any work-in-flight winds
112
+ // down ASAP. `cancelOnce` is idempotent — the finally block
113
+ // will invoke it again after the generator unwinds, but the
114
+ // second call becomes a no-op.
115
+ cancelOnce();
116
+ // Push a synthetic abort marker so the consumer-visible
117
+ // generator breaks out of its loop at the next iteration
118
+ // rather than waiting for a native chunk that will never
119
+ // arrive (e.g. client disconnect before first token).
120
+ queue.push({ aborted: true });
121
+ notify();
122
+ };
123
+ if (signal.aborted) {
124
+ // Signal already fired before we could attach — dispatch
125
+ // synchronously so the waitForItem below resolves immediately.
126
+ triggerAbort();
127
+ }
128
+ else {
129
+ onAbort = triggerAbort;
130
+ signal.addEventListener('abort', onAbort, { once: true });
131
+ }
132
+ }
36
133
  try {
37
- while (true) {
134
+ loop: while (true) {
38
135
  await waitForItem();
39
136
  while (queue.length > 0) {
40
137
  const item = queue.shift();
138
+ if (item.aborted) {
139
+ // Consumer asked us to stop before the next chunk landed.
140
+ // Break out so the finally block runs `handle.cancel()`
141
+ // (idempotent — `triggerAbort` already called it) and the
142
+ // consumer-side `for await` unblocks cleanly. We do NOT
143
+ // throw an AbortError: callers (e.g. server endpoints that
144
+ // flag client disconnect) have already decided this is not
145
+ // an error from their perspective, just an early stop.
146
+ break loop;
147
+ }
41
148
  if (item.error)
42
149
  throw item.error;
43
150
  const chunk = item.chunk;
44
151
  if (chunk.done) {
45
- yield {
152
+ // The native `ChatStreamChunk` carries `cachedTokens` on the
153
+ // terminal (`done == true`) chunk for every streaming entry
154
+ // point. Emit it on the final event verbatim — undefined means
155
+ // the native dispatch did not populate it (e.g. a bridge-level
156
+ // mock or an in-process driver), in which case downstream
157
+ // consumers treat the absence as "unknown / not plumbed" and
158
+ // skip emitting e.g. `X-Cached-Tokens` rather than reporting a
159
+ // fabricated `0`.
160
+ const chunkWithCached = chunk;
161
+ const finalEvent = {
46
162
  text: chunk.text,
47
163
  done: true,
48
164
  finishReason: chunk.finishReason,
49
165
  toolCalls: chunk.toolCalls ?? [],
50
166
  thinking: chunk.thinking ?? null,
51
167
  numTokens: chunk.numTokens,
168
+ promptTokens: chunk.promptTokens ?? 0,
169
+ reasoningTokens: chunk.reasoningTokens ?? 0,
52
170
  rawText: chunk.rawText,
53
171
  performance: chunk.performance ?? undefined,
54
172
  };
173
+ if (typeof chunkWithCached.cachedTokens === 'number') {
174
+ finalEvent.cachedTokens = chunkWithCached.cachedTokens;
175
+ }
176
+ yield finalEvent;
55
177
  return;
56
178
  }
57
- yield { text: chunk.text, done: false };
179
+ const delta = { text: chunk.text, done: false };
180
+ const isReasoning = getNativeIsReasoning(chunk);
181
+ if (isReasoning !== undefined) {
182
+ delta.isReasoning = isReasoning;
183
+ }
184
+ yield delta;
58
185
  }
59
186
  }
60
187
  }
61
188
  finally {
62
- handle.cancel();
189
+ if (signal != null && onAbort != null) {
190
+ try {
191
+ signal.removeEventListener('abort', onAbort);
192
+ }
193
+ catch {
194
+ // removeEventListener shouldn't throw, but stay defensive —
195
+ // a misbehaving signal must not leak out of the finally.
196
+ }
197
+ }
198
+ cancelOnce();
63
199
  }
64
200
  }
65
201
  /**
66
- * Qwen3.5 dense model with AsyncGenerator-based `chatStream()`.
202
+ * Build the streaming-model subclass for a native chat model class.
203
+ *
204
+ * The returned class:
205
+ * - captures the three native callback-based session-streaming methods
206
+ * from `NativeClass.prototype`,
207
+ * - overrides them as `async *` generators delegating to
208
+ * {@link _runChatStream} with identical argument plumbing (including
209
+ * `config ?? null`, `images`, `isError ?? null`, and the `signal`),
210
+ * - overrides `static load` to re-prototype the native instance onto
211
+ * the concrete subclass (`this`) and optionally record the path,
212
+ * - installs a path-backed `applyChatTemplate` when `opts.applyTemplate`
213
+ * (defaulting to `opts.recordModelPath`).
67
214
  *
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
- * ```
215
+ * @internal Exported so the VLM wrapper (`@mlx-node/vlm`) builds its
216
+ * `QianfanOCRModel` from the same factory. Not part of the public API.
75
217
  */
76
- export class Qwen35Model extends Qwen35ModelNative {
77
- static async load(modelPath) {
78
- const instance = await Qwen35ModelNative.load(modelPath);
79
- Object.setPrototypeOf(instance, Qwen35Model.prototype);
80
- return instance;
218
+ export function makeStreamingModel(NativeClass, opts) {
219
+ const recordPath = opts.recordModelPath;
220
+ const applyTemplate = opts.applyTemplate ?? recordPath;
221
+ // Capture the native callback-based methods before the subclass
222
+ // overrides below shadow them on the prototype.
223
+ const nativeStart = NativeClass.prototype.chatStreamSessionStart;
224
+ const nativeContinue = NativeClass.prototype.chatStreamSessionContinue;
225
+ const nativeContinueTool = NativeClass.prototype.chatStreamSessionContinueTool;
226
+ // `NativeClass` is structurally a constructor; cast to a concrete
227
+ // constructor type so `class extends` accepts it. Runtime behavior is
228
+ // unchanged — we extend the real native class.
229
+ const Base = NativeClass;
230
+ class StreamingModelImpl extends Base {
231
+ static async load(modelPath, ...rest) {
232
+ // Forward any trailing family-specific load options verbatim (e.g.
233
+ // Gemma4's `Gemma4LoadOptions` with `draftModelPath`); families whose
234
+ // native `load` takes only the path receive no extras. The public
235
+ // signature is re-narrowed per family via `Parameters<C['load']>` in
236
+ // the factory return type below.
237
+ const instance = await NativeClass.load(modelPath, ...rest);
238
+ // Use `this.prototype` (not `StreamingModelImpl.prototype`) so the
239
+ // concrete subclass declared per family supplies the prototype and
240
+ // `instanceof ConcreteSubclass` holds.
241
+ Object.setPrototypeOf(instance, this.prototype);
242
+ if (recordPath)
243
+ rememberModelPath(instance, modelPath);
244
+ return instance;
245
+ }
246
+ // The native methods are callback-based, but `Base` is typed as a
247
+ // `SessionCapableModel` constructor (whose streaming methods already
248
+ // return `AsyncGenerator<ChatStreamEvent>`), so these overrides are
249
+ // type-compatible and need no `@ts-expect-error` suppression. The
250
+ // callback bridging happens at runtime via the captured natives.
251
+ async *chatStreamSessionStart(messages, config, signal) {
252
+ yield* _runChatStream((callback) => nativeStart.call(this, messages, (config ?? null), callback), signal);
253
+ }
254
+ async *chatStreamSessionContinue(userMessage, images, audio, config, signal) {
255
+ yield* _runChatStream((callback) => nativeContinue.call(this, userMessage, images, audio, (config ?? null), callback), signal);
256
+ }
257
+ async *chatStreamSessionContinueTool(toolCallId, content, config, signal, isError) {
258
+ yield* _runChatStream((callback) => nativeContinueTool.call(this, toolCallId, content, (config ?? null), callback, (isError ?? null)), signal);
259
+ }
81
260
  }
82
- // @ts-expect-error — override callback-based chatStream with AsyncGenerator
83
- async *chatStream(messages, config) {
84
- yield* _createChatStream(_nativeDenseChatStream, this, messages, config);
261
+ if (applyTemplate) {
262
+ Object.defineProperty(StreamingModelImpl.prototype, 'applyChatTemplate', {
263
+ configurable: true,
264
+ writable: true,
265
+ value(messages, addGenerationPrompt, tools, enableThinking) {
266
+ return applyChatTemplateFromModelPath(this, messages, addGenerationPrompt, tools, enableThinking);
267
+ },
268
+ });
85
269
  }
270
+ return StreamingModelImpl;
86
271
  }
87
272
  /**
88
- * Qwen3.5 MoE model with AsyncGenerator-based `chatStream()`.
273
+ * Qwen3.5 dense model with AsyncGenerator-based session streaming.
89
274
  *
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
- * ```
275
+ * The empty `extends` inherits the factory's streaming overrides,
276
+ * `static load`, and `applyChatTemplate`, and supplies the concrete
277
+ * `.name === 'Qwen35Model'` and a working `instanceof`. Records its
278
+ * model path so `applyChatTemplate` can serve a lazily built tokenizer.
97
279
  */
98
- export class Qwen35MoeModel extends Qwen35MoeModelNative {
99
- static async load(modelPath) {
100
- const instance = await Qwen35MoeModelNative.load(modelPath);
101
- Object.setPrototypeOf(instance, Qwen35MoeModel.prototype);
102
- return instance;
103
- }
104
- // @ts-expect-error override callback-based chatStream with AsyncGenerator
105
- async *chatStream(messages, config) {
106
- yield* _createChatStream(_nativeMoeChatStream, this, messages, config);
107
- }
280
+ export class Qwen35Model extends makeStreamingModel(Qwen35ModelNative, { recordModelPath: true }) {
281
+ }
282
+ /** Qwen3.5 MoE model — see {@link Qwen35Model} for the wrapper shape. */
283
+ export class Qwen35MoeModel extends makeStreamingModel(Qwen35MoeModelNative, { recordModelPath: true }) {
284
+ }
285
+ /** LFM2 model (text-only) — see {@link Qwen35Model} for the wrapper shape. */
286
+ export class Lfm2Model extends makeStreamingModel(Lfm2ModelNative, { recordModelPath: true }) {
287
+ }
288
+ /** Gemma4 model (text-only) see {@link Qwen35Model} for the wrapper shape. */
289
+ export class Gemma4Model extends makeStreamingModel(Gemma4ModelNative, { recordModelPath: true }) {
290
+ }
291
+ /**
292
+ * Qwen3 (first-gen, text-only) model.
293
+ *
294
+ * Records its model path (so prototype-set + path-recording match the
295
+ * other families) but does not install the factory's path-backed
296
+ * `applyChatTemplate`; it retains the native tokenizer-backed method.
297
+ */
298
+ export class Qwen3Model extends makeStreamingModel(Qwen3ModelNative, {
299
+ recordModelPath: true,
300
+ applyTemplate: false,
301
+ }) {
302
+ }
303
+ // -------------------------------------------------------------------
304
+ // Compile-time conformance check
305
+ // -------------------------------------------------------------------
306
+ //
307
+ // Ensures each family class structurally satisfies
308
+ // `SessionCapableModel` so `ChatSession<XxxModel>` type-checks in
309
+ // downstream code. Compile-only — the `null as unknown as T`
310
+ // placeholder never runs. If a factory override signature drifts away
311
+ // from the interface, this block fails to compile.
312
+ function _assertSessionCapable() {
313
+ const _qwen35 = null;
314
+ const _moe = null;
315
+ const _lfm2 = null;
316
+ const _gemma4 = null;
317
+ const _qwen3 = null;
318
+ void _qwen35;
319
+ void _moe;
320
+ void _lfm2;
321
+ void _gemma4;
322
+ void _qwen3;
323
+ }
324
+ void _assertSessionCapable;
325
+ /** Compile-time guard that both Qwen3.5 native classes and wrappers retain the exact media planner. */
326
+ function _assertExpandedPromptPlannerSurfaces() {
327
+ const _nativeDense = null;
328
+ const _nativeMoe = null;
329
+ const _wrappedDense = null;
330
+ const _wrappedMoe = null;
331
+ void _nativeDense;
332
+ void _nativeMoe;
333
+ void _wrappedDense;
334
+ void _wrappedMoe;
335
+ }
336
+ void _assertExpandedPromptPlannerSurfaces;
337
+ /** Compile-time guard that the factory preserves every non-streaming native member. */
338
+ function _assertPreservedNativeSurfaces() {
339
+ const _qwen3 = null;
340
+ const _qwen35 = null;
341
+ const _moe = null;
342
+ const _lfm2 = null;
343
+ const _gemma4 = null;
344
+ void _qwen3;
345
+ void _qwen35;
346
+ void _moe;
347
+ void _lfm2;
348
+ void _gemma4;
108
349
  }
350
+ void _assertPreservedNativeSurfaces;
@@ -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"}
@@ -40,7 +40,8 @@
40
40
  * @param description - Description of what the function does
41
41
  * @param properties - Object defining the function parameters (will be JSON stringified)
42
42
  * @param required - Array of required parameter names
43
- * @returns A properly formatted ToolDefinition ready for use with model.chat()
43
+ * @returns A properly formatted ToolDefinition ready to pass via `ChatConfig.tools`
44
+ * on `ChatSession.send()` / `sendToolResult()`.
44
45
  *
45
46
  * @example
46
47
  * ```typescript
@@ -49,12 +50,12 @@
49
50
  * 'Get weather information for a location',
50
51
  * {
51
52
  * location: { type: 'string', description: 'City name' },
52
- * units: { type: 'string', enum: ['celsius', 'fahrenheit'] }
53
+ * units: { type: 'string', enum: ['celsius', 'fahrenheit'] },
53
54
  * },
54
- * ['location']
55
+ * ['location'],
55
56
  * );
56
57
  *
57
- * const result = await model.chat(messages, { tools: [weatherTool] });
58
+ * const result = await session.send(userPrompt, { config: { tools: [weatherTool] } });
58
59
  * ```
59
60
  */
60
61
  export function createToolDefinition(name, description, properties, required) {
@@ -73,29 +74,3 @@ export function createToolDefinition(name, description, properties, required) {
73
74
  },
74
75
  };
75
76
  }
76
- /**
77
- * Format a tool response for inclusion in a message
78
- *
79
- * Creates a properly formatted tool response string that can be used
80
- * in tool messages when continuing a conversation after a tool call.
81
- *
82
- * @param content - The response content (will be JSON stringified if object)
83
- * @returns Formatted tool response string wrapped in `<tool_response>` tags
84
- *
85
- * @example
86
- * ```typescript
87
- * // After executing a tool call from model.chat()
88
- * const toolResult = await executeMyTool(call.arguments);
89
- * const responseMessage = {
90
- * role: 'user',
91
- * content: formatToolResponse(toolResult)
92
- * };
93
- * const finalResult = await model.chat([...messages, responseMessage]);
94
- * ```
95
- */
96
- export function formatToolResponse(content) {
97
- const contentStr = typeof content === 'string' ? content : JSON.stringify(content);
98
- // Qwen3/3.5 expects <tool_response> XML wrapping for tool results.
99
- // Other model families may require a different format.
100
- return `<tool_response>\n${contentStr}\n</tool_response>`;
101
- }