@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/README.md +71 -46
- package/dist/chat-session.d.ts +700 -0
- package/dist/chat-session.d.ts.map +1 -0
- package/dist/chat-session.js +1467 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -6
- package/dist/interfaces.d.ts +2 -7
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/models/lfm2-configs.d.ts +6 -0
- package/dist/models/lfm2-configs.d.ts.map +1 -0
- package/dist/models/lfm2-configs.js +107 -0
- package/dist/models/model-loader.d.ts +194 -4
- package/dist/models/model-loader.d.ts.map +1 -1
- package/dist/models/model-loader.js +253 -21
- package/dist/models/paged-config-override.d.ts +60 -0
- package/dist/models/paged-config-override.d.ts.map +1 -0
- package/dist/models/paged-config-override.js +254 -0
- package/dist/models/qwen3_5-configs.d.ts.map +1 -1
- package/dist/models/qwen3_5-configs.js +5 -0
- package/dist/stream.d.ts +234 -29
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +294 -52
- package/dist/tools/index.d.ts +38 -11
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +38 -11
- package/dist/tools/types.d.ts +5 -25
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +5 -30
- package/package.json +3 -3
package/dist/stream.js
CHANGED
|
@@ -1,19 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
9
|
-
* chatStream into a `for await...of`-compatible stream.
|
|
25
|
+
* Shared AsyncGenerator adapter for callback-based native streaming methods.
|
|
10
26
|
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
69
|
-
*
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
273
|
+
* Qwen3.5 dense model with AsyncGenerator-based session streaming.
|
|
89
274
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,27 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool calling utilities
|
|
2
|
+
* Tool calling utilities
|
|
3
3
|
*
|
|
4
|
-
* Provides types and helpers for working with tool/function calling
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,cAAc,YAAY,CAAC"}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,27 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool calling utilities
|
|
2
|
+
* Tool calling utilities
|
|
3
3
|
*
|
|
4
|
-
* Provides types and helpers for working with tool/function calling
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
*
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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"}
|
package/dist/tools/types.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
}
|