@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/README.md +71 -46
- package/dist/chat-session.d.ts +455 -0
- package/dist/chat-session.d.ts.map +1 -0
- package/dist/chat-session.js +920 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -4
- package/dist/interfaces.d.ts +11 -1
- 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 +48 -0
- package/dist/models/model-loader.d.ts +25 -4
- package/dist/models/model-loader.d.ts.map +1 -1
- package/dist/models/model-loader.js +62 -7
- package/dist/stream.d.ts +154 -26
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +346 -37
- 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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mlx-node/lm
|
|
2
2
|
|
|
3
|
-
High-level language model inference for Node.js on Apple Silicon. Supports Qwen3
|
|
3
|
+
High-level language model inference for Node.js on Apple Silicon. Supports Qwen3, Qwen3.5 (Dense and MoE), LFM2, and Gemma4 with streaming, multi-turn chat sessions, tool calling, and profiling — all running locally on Metal GPU.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
@@ -15,26 +15,31 @@ npm install @mlx-node/lm
|
|
|
15
15
|
|
|
16
16
|
## Quick Start
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
import { loadModel } from '@mlx-node/lm';
|
|
18
|
+
Multi-turn chat runs through `ChatSession`, which owns the server-side KV cache and hides the session bookkeeping behind `send()` / `sendStream()`. The `loadSession()` convenience wrapper loads the model and constructs the session in one step:
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
```typescript
|
|
21
|
+
import { loadSession } from '@mlx-node/lm';
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const session = await loadSession('./models/Qwen3-0.6B');
|
|
24
24
|
|
|
25
|
+
const result = await session.send('What is the capital of France?');
|
|
25
26
|
console.log(result.text);
|
|
27
|
+
|
|
28
|
+
// Follow-ups reuse the live KV cache — no prompt replay.
|
|
29
|
+
const followUp = await session.send('And its population?');
|
|
30
|
+
console.log(followUp.text);
|
|
26
31
|
```
|
|
27
32
|
|
|
28
33
|
## Streaming
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
Every generative model wrapper supports token-by-token streaming via `session.sendStream()`, which yields an `AsyncGenerator<ChatStreamEvent>`:
|
|
31
36
|
|
|
32
37
|
```typescript
|
|
33
|
-
import {
|
|
38
|
+
import { loadSession } from '@mlx-node/lm';
|
|
34
39
|
|
|
35
|
-
const
|
|
40
|
+
const session = await loadSession('./models/Qwen3.5-0.8B');
|
|
36
41
|
|
|
37
|
-
for await (const event of
|
|
42
|
+
for await (const event of session.sendStream('Write a haiku about TypeScript.')) {
|
|
38
43
|
if (!event.done) {
|
|
39
44
|
process.stdout.write(event.text);
|
|
40
45
|
} else {
|
|
@@ -43,16 +48,16 @@ for await (const event of model.chatStream(messages, config)) {
|
|
|
43
48
|
}
|
|
44
49
|
```
|
|
45
50
|
|
|
46
|
-
Breaking out of the loop automatically cancels generation.
|
|
51
|
+
Breaking out of the loop automatically cancels generation. The session tracks its turn state so the next `send()` / `sendStream()` continues the same conversation against the live cache.
|
|
47
52
|
|
|
48
53
|
## Tool Calling
|
|
49
54
|
|
|
50
|
-
OpenAI-compatible function calling with `createToolDefinition
|
|
55
|
+
OpenAI-compatible function calling with `createToolDefinition`. Tool-result turns feed back through the same session via `sendToolResult()`, which dispatches a native `chatSessionContinueTool` against the live KV cache:
|
|
51
56
|
|
|
52
57
|
```typescript
|
|
53
|
-
import {
|
|
58
|
+
import { loadSession, createToolDefinition } from '@mlx-node/lm';
|
|
54
59
|
|
|
55
|
-
const
|
|
60
|
+
const session = await loadSession('./models/Qwen3-0.6B');
|
|
56
61
|
|
|
57
62
|
const tools = [
|
|
58
63
|
createToolDefinition(
|
|
@@ -65,37 +70,52 @@ const tools = [
|
|
|
65
70
|
),
|
|
66
71
|
];
|
|
67
72
|
|
|
68
|
-
const result =
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const result = await session.send('What is the weather in Tokyo?', { config: { tools } });
|
|
74
|
+
|
|
75
|
+
// The chat-session API only supports exactly one tool call per assistant turn:
|
|
76
|
+
// each `sendToolResult` dispatch immediately re-opens the assistant turn, so
|
|
77
|
+
// feeding a second result for the same turn would interleave a new assistant
|
|
78
|
+
// reply between the two results. `ChatSession` enforces this at runtime — a
|
|
79
|
+
// subsequent `sendToolResult*` after a multi-call turn throws with a clear
|
|
80
|
+
// error — and the caller must refuse multi-call turns up front. Tighten the
|
|
81
|
+
// prompt or tool spec so the model emits at most one call per turn.
|
|
82
|
+
const okCalls = result.toolCalls?.filter((tc) => tc.status === 'ok') ?? [];
|
|
83
|
+
if (okCalls.length > 1) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`ChatSession only supports one tool call per assistant turn; ` +
|
|
86
|
+
`model emitted ${okCalls.length}. Tighten the prompt or tool spec.`,
|
|
80
87
|
);
|
|
81
88
|
}
|
|
89
|
+
const call = okCalls[0];
|
|
90
|
+
if (call) {
|
|
91
|
+
const toolOutput = JSON.stringify(await executeMyTool(call));
|
|
92
|
+
const followUp = await session.sendToolResult(call.id, toolOutput, { config: { tools } });
|
|
93
|
+
console.log(followUp.text);
|
|
94
|
+
}
|
|
82
95
|
```
|
|
83
96
|
|
|
84
97
|
## Model Loading
|
|
85
98
|
|
|
86
|
-
`loadModel()` auto-detects the model architecture from `config.json
|
|
99
|
+
`loadModel()` auto-detects the model architecture from `config.json`. Use `loadSession()` when you want an ergonomic `ChatSession` handle in one step, or load a concrete model class and construct `new ChatSession(model)` when you need a reference to both the model and the session (e.g. for `generate()` calls, training, or model metadata):
|
|
87
100
|
|
|
88
101
|
```typescript
|
|
89
|
-
import {
|
|
102
|
+
import { loadSession, ChatSession, Qwen35Model, Qwen35MoeModel } from '@mlx-node/lm';
|
|
90
103
|
|
|
91
|
-
//
|
|
92
|
-
const
|
|
104
|
+
// Convenience: auto-detect architecture and wrap in a ChatSession.
|
|
105
|
+
const session = await loadSession('./models/Qwen3-0.6B', { system: 'Be concise.' });
|
|
93
106
|
|
|
94
|
-
// Or load a specific architecture directly
|
|
107
|
+
// Or load a specific architecture directly — every generative model wrapper
|
|
108
|
+
// structurally satisfies ChatSession's SessionCapableModel bound.
|
|
95
109
|
const dense = await Qwen35Model.load('./models/Qwen3.5-0.8B');
|
|
96
110
|
const moe = await Qwen35MoeModel.load('./models/Qwen3.5-35B-A3B');
|
|
111
|
+
const denseSession = new ChatSession(dense);
|
|
112
|
+
const moeSession = new ChatSession(moe);
|
|
97
113
|
```
|
|
98
114
|
|
|
115
|
+
`loadSession()` rejects embedding models (`HarrierModel`) and the native `QianfanOCRModel` — for the VLM case, import `QianfanOCRModel` from `@mlx-node/vlm` and wrap it with `new ChatSession(...)` directly.
|
|
116
|
+
|
|
117
|
+
`ChatSession` accepts an options bag with `{ system?, defaultConfig? }`. The system prompt is injected on the first turn and never re-sent. Per-call config passed to `send()` / `sendStream()` shallow-merges on top of `defaultConfig`. Call `session.reset()` to wipe the KV cache and start a fresh conversation.
|
|
118
|
+
|
|
99
119
|
### Pre-defined Configs
|
|
100
120
|
|
|
101
121
|
```typescript
|
|
@@ -104,8 +124,8 @@ import { QWEN3_CONFIGS, QWEN35_CONFIGS, getQwen3Config, getQwen35Config } from '
|
|
|
104
124
|
// Available Qwen3 configs: 'qwen3-0.6b', 'qwen3-1.7b', 'qwen3-7b'
|
|
105
125
|
const config = getQwen3Config('qwen3-0.6b');
|
|
106
126
|
|
|
107
|
-
// Available Qwen3.5 configs: 'qwen3.5-0.
|
|
108
|
-
const config35 = getQwen35Config('qwen3.5-0.
|
|
127
|
+
// Available Qwen3.5 configs: 'qwen3.5-0.6b'
|
|
128
|
+
const config35 = getQwen35Config('qwen3.5-0.6b');
|
|
109
129
|
```
|
|
110
130
|
|
|
111
131
|
## Profiling
|
|
@@ -128,12 +148,16 @@ Or set `MLX_PROFILE_DECODE=1` to auto-enable and write a report on exit.
|
|
|
128
148
|
|
|
129
149
|
### Classes
|
|
130
150
|
|
|
131
|
-
| Class | Description
|
|
132
|
-
| ---------------- |
|
|
133
|
-
| `loadModel()` | Auto-detect and load any supported model from disk
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
151
|
+
| Class | Description |
|
|
152
|
+
| ---------------- | --------------------------------------------------------------------------------- |
|
|
153
|
+
| `loadModel()` | Auto-detect and load any supported model from disk |
|
|
154
|
+
| `loadSession()` | `loadModel()` + `new ChatSession(model)` in one step |
|
|
155
|
+
| `ChatSession<M>` | Multi-turn chat wrapper — `send()`, `sendStream()`, `sendToolResult()`, `reset()` |
|
|
156
|
+
| `Qwen3Model` | Qwen3 inference — `generate()`, paged attention, speculative decoding |
|
|
157
|
+
| `Qwen35Model` | Qwen3.5 Dense — `generate()` with compiled C++ forward |
|
|
158
|
+
| `Qwen35MoeModel` | Qwen3.5 MoE — `generate()` with compiled C++ forward and expert routing |
|
|
159
|
+
| `Gemma4Model` | Gemma4 inference — `generate()` |
|
|
160
|
+
| `Lfm2Model` | LFM2.5 hybrid conv+attention inference — `generate()` |
|
|
137
161
|
|
|
138
162
|
### Streaming Types
|
|
139
163
|
|
|
@@ -179,8 +203,6 @@ function createToolDefinition(
|
|
|
179
203
|
properties?: Record<string, FunctionParameterProperty>,
|
|
180
204
|
required?: string[],
|
|
181
205
|
): ToolDefinition;
|
|
182
|
-
|
|
183
|
-
function formatToolResponse(content: string): string;
|
|
184
206
|
```
|
|
185
207
|
|
|
186
208
|
### Functions
|
|
@@ -188,18 +210,21 @@ function formatToolResponse(content: string): string;
|
|
|
188
210
|
| Function | Description |
|
|
189
211
|
| ------------------------ | ---------------------------------------------------- |
|
|
190
212
|
| `createToolDefinition()` | Create an OpenAI-compatible tool definition |
|
|
191
|
-
| `formatToolResponse()` | Wrap tool output in `<tool_response>` tags |
|
|
192
213
|
| `detectModelType()` | Read `config.json` and return the `model_type` field |
|
|
193
214
|
| `enableProfiling()` | Start profiling with auto-report on exit |
|
|
194
215
|
| `disableProfiling()` | Stop profiling and write JSON report |
|
|
195
216
|
|
|
196
217
|
## Supported Models
|
|
197
218
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
| Qwen3
|
|
219
|
+
Every generative model wrapper exposes the same `ChatSession<M>` surface — `send()`, `sendStream()`, and `sendToolResult()` all work against any of the models below.
|
|
220
|
+
|
|
221
|
+
| Model | `generate()` | `ChatSession` | Training | Notes |
|
|
222
|
+
| ------------- | :----------: | :-----------: | :------: | ------------------------------------- |
|
|
223
|
+
| Qwen3 | Yes | Yes | GRPO/SFT | Paged attention, speculative decoding |
|
|
224
|
+
| Qwen3.5 Dense | Yes | Yes | GRPO/SFT | Compiled C++ forward, VLM variant |
|
|
225
|
+
| Qwen3.5 MoE | Yes | Yes | GRPO/SFT | Compiled C++ forward, expert routing |
|
|
226
|
+
| Gemma4 | Yes | Yes | No | Streaming chat via session |
|
|
227
|
+
| LFM2.5 | Yes | Yes | No | Hybrid conv + attention architecture |
|
|
203
228
|
|
|
204
229
|
## Performance
|
|
205
230
|
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic server-side chat session wrapper.
|
|
3
|
+
*
|
|
4
|
+
* `ChatSession<M>` is the cross-model chat-session wrapper. It works
|
|
5
|
+
* against any model that exposes the uniform chat-session NAPI
|
|
6
|
+
* surface — `chatSessionStart`,
|
|
7
|
+
* `chatSessionContinue`, `chatSessionContinueTool`, and their
|
|
8
|
+
* streaming variants plus `resetCaches`. See `SessionCapableModel`
|
|
9
|
+
* below.
|
|
10
|
+
*
|
|
11
|
+
* Design notes:
|
|
12
|
+
*
|
|
13
|
+
* - The session tracks its own `ChatMessage[]` history on the
|
|
14
|
+
* TypeScript side. In the common text-continue case the history
|
|
15
|
+
* is only appended to and never read back — each `send()` on
|
|
16
|
+
* turn >= 1 issues a cheap `chatSessionContinue` delta against
|
|
17
|
+
* the live KV cache. The history is kept purely so the
|
|
18
|
+
* image-change mid-session path can call `chatSessionStart` with
|
|
19
|
+
* the full rebuilt history for a clean re-prefill.
|
|
20
|
+
*
|
|
21
|
+
* - An image hash (`lastImagesKey`) tracks the images bound to the
|
|
22
|
+
* current cache. A `send()` call whose image set has changed
|
|
23
|
+
* (different bytes or different ordering) triggers a full
|
|
24
|
+
* restart: `resetCaches()` → push the new user message (with
|
|
25
|
+
* images) to history → `chatSessionStart(history)`.
|
|
26
|
+
*
|
|
27
|
+
* - Text-only `send()` on turn >= 1 takes the cheap delta path.
|
|
28
|
+
*
|
|
29
|
+
* - `sendToolResult` always dispatches `chatSessionContinueTool`,
|
|
30
|
+
* since tool turns never change image state. The session enforces
|
|
31
|
+
* a strict unresolved-ok-tool-call contract at runtime, driven by
|
|
32
|
+
* `unresolvedOkToolCallCount` (derived from `ChatResult.toolCalls`
|
|
33
|
+
* after each turn via `countOkToolCalls` /
|
|
34
|
+
* `computeTrailingAssistantUnresolvedToolCallCount`):
|
|
35
|
+
*
|
|
36
|
+
* * `null` — the trailing assistant turn has no outstanding ok
|
|
37
|
+
* tool call. Plain `send()` / `sendStream()` are the only
|
|
38
|
+
* valid entry points; `sendToolResult*()` throws because
|
|
39
|
+
* there is nothing for the result to resolve.
|
|
40
|
+
* * `1` — exactly one outstanding ok tool call. Plain `send()` /
|
|
41
|
+
* `sendStream()` throw (they would orphan the call);
|
|
42
|
+
* `sendToolResult*()` is the sole valid forward step and
|
|
43
|
+
* dispatches the tool result through the native session.
|
|
44
|
+
* * `>1` — a multi-tool-call fan-out that the chat-session API
|
|
45
|
+
* cannot progress incrementally (each `sendToolResult*` would
|
|
46
|
+
* re-open the assistant turn and weave new replies between
|
|
47
|
+
* the sibling results). Both `send()` / `sendStream()` and
|
|
48
|
+
* `sendToolResult*()` throw. The only valid recovery is
|
|
49
|
+
* `reset()` or `primeHistory()` + `startFromHistory*()` with
|
|
50
|
+
* a fully-resolved conversation — there is no "advance past
|
|
51
|
+
* the broken turn" path. This mirrors the native ChatML delta
|
|
52
|
+
* format which would otherwise silently corrupt multi-call
|
|
53
|
+
* conversations.
|
|
54
|
+
*
|
|
55
|
+
* - `sawFinal` gates `turnCount` advance on the streaming path, so
|
|
56
|
+
* the session refuses to advance when the stream throws
|
|
57
|
+
* mid-decode or yields a final chunk with
|
|
58
|
+
* `finishReason: 'error'`.
|
|
59
|
+
*
|
|
60
|
+
* - The `inFlight` guard rejects concurrent `send()` /
|
|
61
|
+
* `sendStream()` calls at the class level. The native side
|
|
62
|
+
* serializes cache mutation on a single worker thread, so a
|
|
63
|
+
* second in-flight call would race the first's cache-save step.
|
|
64
|
+
*
|
|
65
|
+
* - **Cold-restart primitives.** `primeHistory()` plus
|
|
66
|
+
* `startFromHistory()` / `startFromHistoryStream()` let a caller
|
|
67
|
+
* seed a fresh session with an externally-reconstructed history
|
|
68
|
+
* (e.g. a server `ResponseStore` chain) and replay it through the
|
|
69
|
+
* native `chatSessionStart` path without going through `send()`.
|
|
70
|
+
* These are intended for server-side `SessionRegistry` cache-miss
|
|
71
|
+
* cold-start; normal usage stays on `send` / `sendStream` /
|
|
72
|
+
* `sendToolResult` / `reset`.
|
|
73
|
+
*
|
|
74
|
+
* ## Typical usage
|
|
75
|
+
*
|
|
76
|
+
* ```typescript
|
|
77
|
+
* import { Qwen35Model, ChatSession } from '@mlx-node/lm';
|
|
78
|
+
*
|
|
79
|
+
* const model = await Qwen35Model.load('./models/qwen3.5-0.8b');
|
|
80
|
+
* const session = new ChatSession(model, { system: 'Be concise.' });
|
|
81
|
+
* const r1 = await session.send('Say hi in one word.');
|
|
82
|
+
* const r2 = await session.send('Another word?');
|
|
83
|
+
* await session.reset();
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
import type { ChatConfig, ChatMessage, ChatResult } from '@mlx-node/core';
|
|
87
|
+
import type { ChatStreamEvent } from './stream.js';
|
|
88
|
+
/**
|
|
89
|
+
* Structural interface matched by every generative model wrapper
|
|
90
|
+
* (`Qwen35Model`, `Qwen35MoeModel`, `Lfm2Model`, `Gemma4Model`,
|
|
91
|
+
* `Qwen3Model`, and the Qianfan-OCR VLM wrapper). `ChatSession<M>` is
|
|
92
|
+
* generic over `M extends SessionCapableModel` so each session
|
|
93
|
+
* instance statically binds to a specific model's concrete type
|
|
94
|
+
* (handy for IDE autocomplete) while the implementation remains
|
|
95
|
+
* fully structural.
|
|
96
|
+
*/
|
|
97
|
+
export interface SessionCapableModel {
|
|
98
|
+
chatSessionStart(messages: ChatMessage[], config?: ChatConfig | null): Promise<ChatResult>;
|
|
99
|
+
chatSessionContinue(userMessage: string, images: Uint8Array[] | null, config?: ChatConfig | null): Promise<ChatResult>;
|
|
100
|
+
chatSessionContinueTool(toolCallId: string, content: string, config?: ChatConfig | null): Promise<ChatResult>;
|
|
101
|
+
/**
|
|
102
|
+
* The optional `signal` parameter on every streaming entry point is
|
|
103
|
+
* plumbed into the `_runChatStream` fast-abort path in the wrapper
|
|
104
|
+
* implementations. Callers that need client-disconnect-aware
|
|
105
|
+
* cancellation (e.g. HTTP endpoints flipping an AbortController on
|
|
106
|
+
* socket close) can attach one here and the native decode unwinds
|
|
107
|
+
* at the next safepoint via `ChatStreamHandle.cancel()`. Non-signal
|
|
108
|
+
* callers (the common direct-use path) just omit it.
|
|
109
|
+
*/
|
|
110
|
+
chatStreamSessionStart(messages: ChatMessage[], config?: ChatConfig | null, signal?: AbortSignal): AsyncGenerator<ChatStreamEvent>;
|
|
111
|
+
chatStreamSessionContinue(userMessage: string, images: Uint8Array[] | null, config?: ChatConfig | null, signal?: AbortSignal): AsyncGenerator<ChatStreamEvent>;
|
|
112
|
+
chatStreamSessionContinueTool(toolCallId: string, content: string, config?: ChatConfig | null, signal?: AbortSignal): AsyncGenerator<ChatStreamEvent>;
|
|
113
|
+
resetCaches(): void;
|
|
114
|
+
}
|
|
115
|
+
/** Per-call options for {@link ChatSession#send} / `sendStream`. */
|
|
116
|
+
export interface SendOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Optional image bytes attached to this user turn. When the image
|
|
119
|
+
* set differs from the session's current `lastImagesKey`, the
|
|
120
|
+
* session forcibly restarts via `chatSessionStart`.
|
|
121
|
+
*/
|
|
122
|
+
images?: Uint8Array[];
|
|
123
|
+
/**
|
|
124
|
+
* Per-call `ChatConfig` overlay applied on top of the session's
|
|
125
|
+
* `defaultConfig`. `reuseCache` is always forced on regardless of
|
|
126
|
+
* what the caller passes.
|
|
127
|
+
*/
|
|
128
|
+
config?: ChatConfig;
|
|
129
|
+
/**
|
|
130
|
+
* Optional AbortSignal plumbed into the streaming fast-abort path.
|
|
131
|
+
*
|
|
132
|
+
* Only honored by the streaming entry points (`sendStream`,
|
|
133
|
+
* `sendToolResultStream`, `startFromHistoryStream`) — the
|
|
134
|
+
* non-streaming `send` / `sendToolResult` / `startFromHistory`
|
|
135
|
+
* calls have NO native cancel surface, so a signal passed to them
|
|
136
|
+
* is ignored. Pass one here and the inner `_runChatStream`
|
|
137
|
+
* adapter wakes from `waitForItem()` on abort, calls
|
|
138
|
+
* `handle.cancel()` on the native handle, and unwinds the stream
|
|
139
|
+
* without throwing an AbortError — the outer consumer's `for await`
|
|
140
|
+
* just ends early. Intended for HTTP endpoints that flip a
|
|
141
|
+
* controller on `res.once('close', …)` so client disconnect stops
|
|
142
|
+
* the native decode at the next safepoint rather than running it
|
|
143
|
+
* to completion under the per-model mutex.
|
|
144
|
+
*/
|
|
145
|
+
signal?: AbortSignal;
|
|
146
|
+
}
|
|
147
|
+
/** Constructor options for {@link ChatSession}. */
|
|
148
|
+
export interface ChatSessionOptions {
|
|
149
|
+
/**
|
|
150
|
+
* Optional system prompt prepended as the first message on turn 1.
|
|
151
|
+
* Subsequent turns don't re-inject the system prompt — the cache
|
|
152
|
+
* already holds it.
|
|
153
|
+
*/
|
|
154
|
+
system?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Default `ChatConfig` applied to every `send()` / `sendStream()`
|
|
157
|
+
* / `sendToolResult()` call. Per-call config is shallow-merged on
|
|
158
|
+
* top of this, and `reuseCache` is forced on.
|
|
159
|
+
*/
|
|
160
|
+
defaultConfig?: ChatConfig;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Cross-model chat session. See module docstring for design notes.
|
|
164
|
+
*
|
|
165
|
+
* The generic parameter `M` statically captures the concrete model
|
|
166
|
+
* type so the structural interface stays as expressive as the
|
|
167
|
+
* concrete one. Internally the class only uses the
|
|
168
|
+
* `SessionCapableModel` surface.
|
|
169
|
+
*/
|
|
170
|
+
export declare class ChatSession<M extends SessionCapableModel = SessionCapableModel> {
|
|
171
|
+
private readonly model;
|
|
172
|
+
private readonly system;
|
|
173
|
+
private readonly defaultConfig;
|
|
174
|
+
/**
|
|
175
|
+
* Full conversation history tracked on the TS side. Appended to on
|
|
176
|
+
* every successful turn. Only read back when the image-change path
|
|
177
|
+
* triggers a restart — normal text continues use the server-side
|
|
178
|
+
* cache, not this array.
|
|
179
|
+
*/
|
|
180
|
+
private history;
|
|
181
|
+
/**
|
|
182
|
+
* Hex-encoded byte-identity key of the image set currently bound
|
|
183
|
+
* to the server's KV cache (FNV-1a 64-bit; see `computeImagesKey`).
|
|
184
|
+
* `null` when no images are cached. A `send()` whose new key
|
|
185
|
+
* differs triggers a full `chatSessionStart` restart.
|
|
186
|
+
*/
|
|
187
|
+
private lastImagesKey;
|
|
188
|
+
private turnCount;
|
|
189
|
+
private inFlight;
|
|
190
|
+
/**
|
|
191
|
+
* Count of `ok` tool calls emitted by the prior assistant turn, or
|
|
192
|
+
* `null` when the prior turn produced none. Gates every continuation
|
|
193
|
+
* entry point on the tool-call resolution invariant because each
|
|
194
|
+
* native `chat_session_continue*` dispatch re-opens the assistant
|
|
195
|
+
* turn:
|
|
196
|
+
*
|
|
197
|
+
* - A plain text `send` / `sendStream` after ANY outstanding tool
|
|
198
|
+
* call would orphan the call(s) by weaving a fresh user turn
|
|
199
|
+
* between the assistant's `tool_call` and any response.
|
|
200
|
+
* - A `sendToolResult` / `sendToolResultStream` is only servable
|
|
201
|
+
* when exactly one tool call is outstanding. A multi-call
|
|
202
|
+
* fan-out (`> 1`) cannot be resolved one result at a time — the
|
|
203
|
+
* siblings would be separated by fresh assistant replies — so
|
|
204
|
+
* those entry points also reject.
|
|
205
|
+
*
|
|
206
|
+
* Cleared on every successful commit whose new turn emits zero `ok`
|
|
207
|
+
* tool calls, and on `reset()`. See `assertCanSendPlain` /
|
|
208
|
+
* `assertCanSendToolResult` for the per-entry-point gate logic.
|
|
209
|
+
*/
|
|
210
|
+
private unresolvedOkToolCallCount;
|
|
211
|
+
constructor(model: M, options?: ChatSessionOptions);
|
|
212
|
+
/**
|
|
213
|
+
* Number of completed turns. Increments only after a successful
|
|
214
|
+
* round-trip — in-flight or failed calls leave this untouched.
|
|
215
|
+
*/
|
|
216
|
+
get turns(): number;
|
|
217
|
+
/** Whether the session currently has images bound to its cache. */
|
|
218
|
+
get hasImages(): boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Count of `ok` tool calls from the most recent assistant turn, or
|
|
221
|
+
* `null` when the trailing turn produced none. Non-null means the
|
|
222
|
+
* session is parked on an unresolved tool-call turn and the only
|
|
223
|
+
* forward-progress move is `sendToolResult*()` against one of the
|
|
224
|
+
* outstanding ids — and only when the count is exactly 1. A
|
|
225
|
+
* multi-call fan-out (`> 1`) cannot be served by the chat-session
|
|
226
|
+
* API at all; server endpoints should pre-check this getter and
|
|
227
|
+
* route around a fan-out via `reset()` + `primeHistory()` +
|
|
228
|
+
* `startFromHistory()` cold replay that resolves every sibling in
|
|
229
|
+
* one atomic jinja render.
|
|
230
|
+
*
|
|
231
|
+
* The flag updates after every successful `send` / `sendStream` /
|
|
232
|
+
* `sendToolResult` / `sendToolResultStream` / `startFromHistory*`
|
|
233
|
+
* commit, and after `primeHistory()` (from the trailing assistant
|
|
234
|
+
* message's `toolCalls.length`). `reset()` clears it.
|
|
235
|
+
*/
|
|
236
|
+
get pendingUnresolvedToolCallCount(): number | null;
|
|
237
|
+
/**
|
|
238
|
+
* Send a user message and resolve with the assistant reply.
|
|
239
|
+
*
|
|
240
|
+
* Turn 0 and any turn whose image set has changed dispatch through
|
|
241
|
+
* `chatSessionStart` with the full history. All other turns use
|
|
242
|
+
* the cheap `chatSessionContinue` delta path.
|
|
243
|
+
*/
|
|
244
|
+
send(userMessage: string, opts?: SendOptions): Promise<ChatResult>;
|
|
245
|
+
/**
|
|
246
|
+
* Streaming variant of {@link ChatSession#send}.
|
|
247
|
+
*
|
|
248
|
+
* Routing matches `send()`. The assistant reply is accumulated
|
|
249
|
+
* from stream deltas and pushed to `history` only after a
|
|
250
|
+
* successful terminal chunk (`done: true` with non-error
|
|
251
|
+
* `finishReason`). Caller break, mid-stream exceptions, and error
|
|
252
|
+
* finishes all leave `turnCount` untouched and the history
|
|
253
|
+
* un-appended for the turn so the next call re-routes through the
|
|
254
|
+
* start path.
|
|
255
|
+
*/
|
|
256
|
+
sendStream(userMessage: string, opts?: SendOptions): AsyncGenerator<ChatStreamEvent>;
|
|
257
|
+
/**
|
|
258
|
+
* Send a tool-result turn. Always dispatches
|
|
259
|
+
* `chatSessionContinueTool` — tool turns never change image state,
|
|
260
|
+
* so there is no restart path here.
|
|
261
|
+
*
|
|
262
|
+
* Rejects if the prior assistant turn emitted more than one `ok`
|
|
263
|
+
* tool call: the chat-session API only supports exactly one tool
|
|
264
|
+
* call per assistant turn because each `sendToolResult` dispatch
|
|
265
|
+
* immediately re-opens the assistant turn, so responding to the
|
|
266
|
+
* remaining calls would interleave new assistant replies between
|
|
267
|
+
* the results and corrupt the conversation structure. Callers that
|
|
268
|
+
* hit this must tighten the prompt / tool spec or reset the
|
|
269
|
+
* session.
|
|
270
|
+
*
|
|
271
|
+
* Appends a `{ role: 'tool', ... }` message to history on success.
|
|
272
|
+
*/
|
|
273
|
+
sendToolResult(toolCallId: string, content: string, opts?: {
|
|
274
|
+
config?: ChatConfig;
|
|
275
|
+
}): Promise<ChatResult>;
|
|
276
|
+
/** Streaming variant of {@link ChatSession#sendToolResult}. */
|
|
277
|
+
sendToolResultStream(toolCallId: string, content: string, opts?: {
|
|
278
|
+
config?: ChatConfig;
|
|
279
|
+
signal?: AbortSignal;
|
|
280
|
+
}): AsyncGenerator<ChatStreamEvent>;
|
|
281
|
+
/**
|
|
282
|
+
* Reset the session state.
|
|
283
|
+
*
|
|
284
|
+
* Clears the underlying model's KV caches and wipes local history,
|
|
285
|
+
* image key, and turn counter so the next `send()` goes through
|
|
286
|
+
* `chatSessionStart` again.
|
|
287
|
+
*
|
|
288
|
+
* Returns `Promise<void>` for an async-friendly signature even
|
|
289
|
+
* though `resetCaches()` is currently synchronous.
|
|
290
|
+
*/
|
|
291
|
+
reset(): Promise<void>;
|
|
292
|
+
/**
|
|
293
|
+
* Prime the session history without running inference.
|
|
294
|
+
*
|
|
295
|
+
* Used by the server-side `SessionRegistry` cold-start fallback: when
|
|
296
|
+
* a request arrives with a `previous_response_id` that the cache has
|
|
297
|
+
* missed, the endpoint reconstructs the full conversation from the
|
|
298
|
+
* `ResponseStore` and primes a fresh session with it, then calls
|
|
299
|
+
* `startFromHistory()` to replay it through the native KV cache.
|
|
300
|
+
*
|
|
301
|
+
* Rejects if the session is in flight or has already taken a turn.
|
|
302
|
+
* Replaces the internal history with a shallow copy of `messages`.
|
|
303
|
+
*/
|
|
304
|
+
primeHistory(messages: ChatMessage[]): void;
|
|
305
|
+
/**
|
|
306
|
+
* Run a cold-start `chatSessionStart` using the currently primed
|
|
307
|
+
* history.
|
|
308
|
+
*
|
|
309
|
+
* Intended pairing with {@link primeHistory}: call
|
|
310
|
+
* `primeHistory(fullHistory)` first, then `startFromHistory()` to
|
|
311
|
+
* replay the conversation through the native chat-session API. The
|
|
312
|
+
* final history entry must be a user or tool turn — this is what the
|
|
313
|
+
* native side treats as the "current input" to generate against.
|
|
314
|
+
*
|
|
315
|
+
* Pushes the assistant reply onto the history, advances `turnCount`
|
|
316
|
+
* to 1, and computes `lastImagesKey` from the most recent user
|
|
317
|
+
* message that carries images (so subsequent text-only continues
|
|
318
|
+
* stay on the delta path, and subsequent image turns correctly
|
|
319
|
+
* trigger restart).
|
|
320
|
+
*/
|
|
321
|
+
startFromHistory(config?: ChatConfig): Promise<ChatResult>;
|
|
322
|
+
/**
|
|
323
|
+
* Streaming counterpart to {@link startFromHistory}.
|
|
324
|
+
*
|
|
325
|
+
* Iterates `model.chatStreamSessionStart(history.slice(), config)`,
|
|
326
|
+
* accumulates text, and only commits history + `turnCount` +
|
|
327
|
+
* `lastImagesKey` in the `finally` block when a successful terminal
|
|
328
|
+
* chunk was observed (`done: true` with non-error finishReason).
|
|
329
|
+
* Because history is primed (not appended to), rollback on failure
|
|
330
|
+
* is a no-op: the primed state stays intact so the caller can retry.
|
|
331
|
+
*/
|
|
332
|
+
startFromHistoryStream(config?: ChatConfig, signal?: AbortSignal): AsyncGenerator<ChatStreamEvent>;
|
|
333
|
+
/**
|
|
334
|
+
* Gate plain-text continuation entry points (`send`, `sendStream`)
|
|
335
|
+
* on the tool-call resolution invariant. Any outstanding `ok` tool
|
|
336
|
+
* call from the prior assistant turn — single or multi — makes a
|
|
337
|
+
* plain text continuation unsafe: the native chat-session API
|
|
338
|
+
* re-opens the assistant turn on each continue, so a new user delta
|
|
339
|
+
* would weave a fresh user message between the assistant's
|
|
340
|
+
* `tool_call` and any response, orphaning the call. Callers must
|
|
341
|
+
* resolve outstanding calls via `sendToolResult*()` (single-call
|
|
342
|
+
* case) or re-enter via `reset()` + `primeHistory()` +
|
|
343
|
+
* `startFromHistory()` with a resolved conversation (multi-call
|
|
344
|
+
* fan-out). `reset()` clears the flag and `startFromHistory*`
|
|
345
|
+
* overwrites it via `recordToolCallFanout` on the new response, so
|
|
346
|
+
* legitimate recovery paths are unaffected.
|
|
347
|
+
*/
|
|
348
|
+
private assertCanSendPlain;
|
|
349
|
+
/**
|
|
350
|
+
* Gate tool-result entry points (`sendToolResult`,
|
|
351
|
+
* `sendToolResultStream`) on the single-tool-call-per-turn
|
|
352
|
+
* invariant. Exactly one outstanding tool call is servable — that
|
|
353
|
+
* is the case these methods exist for.
|
|
354
|
+
*
|
|
355
|
+
* Zero outstanding calls (`null`) is also unservable: without a
|
|
356
|
+
* preceding assistant turn that emitted a tool call, a tool-result
|
|
357
|
+
* dispatch would synthesize a `<tool_response>` delta for a call
|
|
358
|
+
* that never existed, corrupting the conversation structure. The
|
|
359
|
+
* native backends do not authenticate `tool_call_id` against prior
|
|
360
|
+
* state — several simply append the tool-response delta verbatim —
|
|
361
|
+
* so rejecting here is the only gate that prevents forged tool
|
|
362
|
+
* state from reaching the model. Callers that want to start a
|
|
363
|
+
* conversation on a resolved tool turn must prime an unresolved
|
|
364
|
+
* single-call assistant turn via `primeHistory()` +
|
|
365
|
+
* `startFromHistory()` first.
|
|
366
|
+
*
|
|
367
|
+
* A multi-call fan-out (`> 1`) cannot be resolved one result at a
|
|
368
|
+
* time because each `sendToolResult` dispatch immediately re-opens
|
|
369
|
+
* the assistant turn, so responding to the siblings would
|
|
370
|
+
* interleave new assistant replies between the results.
|
|
371
|
+
*/
|
|
372
|
+
private assertCanSendToolResult;
|
|
373
|
+
/**
|
|
374
|
+
* Inspect a just-committed turn's tool calls and store the count of
|
|
375
|
+
* `ok` entries in `unresolvedOkToolCallCount`. Any non-zero count
|
|
376
|
+
* parks the session on an unresolved tool-call turn, which gates
|
|
377
|
+
* the next entry point:
|
|
378
|
+
*
|
|
379
|
+
* - count === 0 → flag is `null`: `send`/`sendStream` ok,
|
|
380
|
+
* `sendToolResult*` throws (no outstanding call to resolve)
|
|
381
|
+
* - count === 1 → `send`/`sendStream` throw; `sendToolResult*` ok
|
|
382
|
+
* - count > 1 → every entry point throws (fan-out unservable)
|
|
383
|
+
*
|
|
384
|
+
* See `assertCanSendPlain` / `assertCanSendToolResult` for the full
|
|
385
|
+
* rationale.
|
|
386
|
+
*/
|
|
387
|
+
private recordToolCallFanout;
|
|
388
|
+
/**
|
|
389
|
+
* Merge default + per-call config and force `reuseCache: true`.
|
|
390
|
+
* The session path is a session-reuse operation by construction —
|
|
391
|
+
* `reuseCache: false` on the continue path would wipe the very
|
|
392
|
+
* cache the delta depends on.
|
|
393
|
+
*/
|
|
394
|
+
private mergeConfig;
|
|
395
|
+
/**
|
|
396
|
+
* Shared start-path logic for `send()`. Handles both the turn-0
|
|
397
|
+
* first-ever-send case and the image-change mid-session restart
|
|
398
|
+
* case. The image-change restart preserves prior history so the
|
|
399
|
+
* native side gets the full conversation re-rendered with the new
|
|
400
|
+
* image set.
|
|
401
|
+
*/
|
|
402
|
+
private runStartPath;
|
|
403
|
+
/** Streaming counterpart to {@link runStartPath}. */
|
|
404
|
+
private runStartStreamPath;
|
|
405
|
+
/**
|
|
406
|
+
* Shared pre-start bookkeeping for both `send()` and `sendStream()`:
|
|
407
|
+
*
|
|
408
|
+
* - On an image-change restart (turn >= 1), reset the native KV
|
|
409
|
+
* caches so the new image set gets a fresh prefill. History is
|
|
410
|
+
* intentionally preserved — `chatSessionStart` receives the full
|
|
411
|
+
* accumulated conversation plus the new user turn so the jinja
|
|
412
|
+
* render walks every prior turn and every prior image again
|
|
413
|
+
* (see plan's Turn 3 example: "full jinja on 3-turn history +
|
|
414
|
+
* image B"). `lastImagesKey` will be overwritten by the
|
|
415
|
+
* successful start path right after, and `turnCount` is
|
|
416
|
+
* incremented by the start path the same way as for any other
|
|
417
|
+
* turn.
|
|
418
|
+
* - On a fresh / reset history, re-inject the system prompt.
|
|
419
|
+
*/
|
|
420
|
+
private prepareStartPath;
|
|
421
|
+
/** Build a user `ChatMessage` with or without attached images. */
|
|
422
|
+
private buildUserMessage;
|
|
423
|
+
/**
|
|
424
|
+
* Walk the history backward to find the most recent user message
|
|
425
|
+
* with images and return its FNV-1a key. Used by
|
|
426
|
+
* {@link startFromHistory} and {@link startFromHistoryStream} to
|
|
427
|
+
* hydrate `lastImagesKey` after a cold replay, so subsequent delta
|
|
428
|
+
* continues correctly detect image changes.
|
|
429
|
+
*/
|
|
430
|
+
private computeTrailingImagesKey;
|
|
431
|
+
/**
|
|
432
|
+
* Derive the post-prime value of `unresolvedOkToolCallCount` from
|
|
433
|
+
* the primed history. Walks backward to the most recent assistant
|
|
434
|
+
* turn, then walks forward from that assistant to the end of history
|
|
435
|
+
* subtracting any `tool:` message that references one of the turn's
|
|
436
|
+
* `call_id`s. A fully-resolved history (every outstanding id matched
|
|
437
|
+
* by a sibling `tool:` message) returns `null`; any leftover count is
|
|
438
|
+
* the number of still-unresolved tool calls.
|
|
439
|
+
*
|
|
440
|
+
* Matches the runtime `recordToolCallFanout` semantics on the hot
|
|
441
|
+
* path: zero unresolved → `null` (no pending obligation); one →
|
|
442
|
+
* `1` (servable via `sendToolResult*()` only); two or more → the
|
|
443
|
+
* count itself (unservable fan-out — must be resolved via cold
|
|
444
|
+
* replay). The distinction between "ok" vs. other statuses only
|
|
445
|
+
* exists in the live `ToolCallResult[]` emitted by the native side —
|
|
446
|
+
* the persisted `ChatMessage.toolCalls` on an assistant message only
|
|
447
|
+
* carries successfully parsed calls (i.e. what would have been "ok"
|
|
448
|
+
* in the original live turn), so counting the array length is
|
|
449
|
+
* equivalent. Tool calls whose `id` is missing or empty can't be
|
|
450
|
+
* matched against subsequent `tool_call_id`s, so in that case we
|
|
451
|
+
* fall back to returning the raw `calls.length` (err safe).
|
|
452
|
+
*/
|
|
453
|
+
private computeTrailingAssistantUnresolvedToolCallCount;
|
|
454
|
+
}
|
|
455
|
+
//# sourceMappingURL=chat-session.d.ts.map
|