@mlx-node/core 0.0.7 → 0.0.9

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.
Files changed (3) hide show
  1. package/index.cjs +91 -69
  2. package/index.d.cts +1459 -879
  3. package/package.json +6 -5
package/index.d.cts CHANGED
@@ -119,82 +119,100 @@ export declare class DocUnwarpModel {
119
119
  * commands via channels and await responses.
120
120
  */
121
121
  export declare class Gemma4Model {
122
+ /**
123
+ * Create an uninitialized `Gemma4Model` stub from a config.
124
+ *
125
+ * **Prefer [`Gemma4Model::load`]** for any real usage — `new(config)`
126
+ * is a config-only stub that matches the OCR-model pattern
127
+ * (`VLModel::new(config)`, `QianfanOCRModel::new(config)`) and is
128
+ * intentionally NOT runnable. It was introduced in the cache-limit
129
+ * coordinator work so that the coordinator's per-model delta is
130
+ * registered exclusively on the `load()` path, eliminating a
131
+ * baseline-registration gap where a no-op `new(config)` would have
132
+ * leaked an empty guard into the coordinator.
133
+ *
134
+ * This path does NOT spawn a model thread, NOT materialize any
135
+ * weights, and NOT register with the cache-limit coordinator. The
136
+ * returned instance is only useful for config inspection — every
137
+ * session method (`chatSessionStart` / `chatSessionContinue` /
138
+ * `chatSessionContinueTool` and their streaming variants) rejects
139
+ * with a `napi::Error` whose message is exactly
140
+ * `"Model not initialized. Call Gemma4Model.load() first."` until
141
+ * `load()` runs and installs the underlying model thread. The
142
+ * synchronous `resetCaches()` call is a silent no-op on the stub
143
+ * to keep `ChatSession.reset()` idempotent across both runnable
144
+ * and stub instances.
145
+ *
146
+ * A runnable model requires `await Gemma4Model.load(path)`. The
147
+ * constructor signature is fixed by NAPI-RS; the stub-only behavior is
148
+ * covered by the regression tests in
149
+ * `__test__/models/model-loader-gemma4.test.ts`.
150
+ */
122
151
  constructor(config: Gemma4Config);
152
+ /** Returns true if weights have been loaded via `load()`. */
153
+ get isInitialized(): boolean;
154
+ /**
155
+ * Whether the block-paged KV cache adapter is active on this model
156
+ * instance.
157
+ *
158
+ * `true` iff `Gemma4Inner::paged_adapter` was successfully
159
+ * constructed at load time (driven by
160
+ * `Gemma4Config::use_block_paged_cache`). The
161
+ * `gemma4_paged_vs_flat_parity` integration test pins greedy
162
+ * byte-equal at BF16 against real Gemma-4-E2B-IT weights. Stubs
163
+ * constructed via `new(config)` always return `false`. Surfaced
164
+ * through this NAPI method so server endpoints can branch on it
165
+ * without a model-thread roundtrip.
166
+ */
167
+ hasBlockPagedCache(): boolean;
168
+ /**
169
+ * Whether this loaded instance can execute image-bearing chat turns.
170
+ * Config-only stubs and incomplete/non-paged physical paths return false.
171
+ */
172
+ supportsImages(): boolean;
123
173
  modelId(): number;
124
- /** Load a Gemma4 model from a directory. */
125
- static load(modelPath: string): Promise<Gemma4Model>;
126
174
  /**
127
- * Reset all caches and clear cached token history. Exposed so
128
- * tests and session-management code can start from a known clean
129
- * state between turns.
175
+ * Whether a draft model DSpark or Google assistant — is loaded on
176
+ * this instance (via `Gemma4LoadOptions::draft_model_path`), enabling
177
+ * the speculative-decode whole-turn path.
130
178
  *
131
- * Synchronous on the NAPI boundary every other `SessionCapableModel`
132
- * exposes `resetCaches(): void` and the `ChatSession<M>` cross-model
133
- * wrapper calls this inline during the image-change restart and
134
- * `reset()` flows. Running it as an async NAPI method would break
135
- * that contract and silently drop reset failures because
136
- * `ChatSession.reset()` and the session-start restart path invoke
137
- * `model.resetCaches()` without awaiting.
179
+ * Note: this only reports draft availability. Whether speculative
180
+ * decoding actually runs on a given call also requires the per-request
181
+ * `enableMtp` flag. Named `hasMtpWeights` for parity with the Qwen3.5
182
+ * surface, but it reports an external draft model (either variant),
183
+ * not in-checkpoint MTP heads. Stubs from `new(config)` always return
184
+ * `false`.
185
+ */
186
+ hasMtpWeights(): boolean;
187
+ /** Load a Gemma4 model from a directory. */
188
+ static load(modelPath: string, options?: Gemma4LoadOptions | undefined | null): Promise<Gemma4Model>;
189
+ /**
190
+ * Reset all caches and clear cached token history. Exposed
191
+ * so tests and session-management code can start from a
192
+ * known clean state between turns.
138
193
  */
139
194
  resetCaches(): void;
140
195
  /**
141
196
  * Start a new chat session.
142
197
  *
143
- * Runs the full jinja chat template once, decodes until Gemma4's
144
- * `<turn|>` delimiter, and leaves the KV caches on a clean turn
145
- * boundary so subsequent `chatSessionContinue` /
146
- * `chatSessionContinueTool` calls can append a raw delta on top
147
- * without re-rendering the chat template.
198
+ * Renders the complete conversation through the loaded chat
199
+ * template, decodes until the family's session stop token, and
200
+ * preserves the resulting KV state for exact-prefix reuse.
148
201
  */
149
202
  chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
150
203
  /**
151
- * Continue an existing chat session with a new user message.
152
- *
153
- * Appends a raw Gemma4 user/model delta to the session's cached KV
154
- * state, then decodes the model reply. Stops on `<turn|>` so the
155
- * cache remains on a clean turn boundary for the next turn.
156
- *
157
- * Requires a live session started via `chatSessionStart`. Errors
158
- * if the session is empty, carries image state, or if
159
- * `config.reuse_cache` is explicitly set to `false`.
160
- *
161
- * `images` is an opt-in guard parameter: when non-empty the native
162
- * side returns an error whose message begins with
163
- * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
164
- * `ChatSession` layer can catch the prefix and route image-changes
165
- * back through a fresh `chatSessionStart` uniformly across all
166
- * model backends.
204
+ * Continue an existing chat session from the complete
205
+ * structured conversation. The loaded model template is the
206
+ * sole authority for the rendered suffix; native cache reuse
207
+ * occurs only after the completed structured history is verified
208
+ * against the saved token history.
167
209
  */
168
- chatSessionContinue(
169
- userMessage: string,
170
- images: Uint8Array[] | null | undefined,
171
- config: ChatConfig | null | undefined,
172
- ): Promise<ChatResult>;
173
- /**
174
- * Continue an existing chat session with a tool-result turn.
175
- *
176
- * Builds a Gemma4-format tool delta
177
- * (`
178
- <|turn>tool
179
- {content}<turn|>
180
- <|turn>model
181
- `) from
182
- * `content` and prefills it on top of the live session caches,
183
- * then decodes the model reply. Stops on `<turn|>` so the cache
184
- * stays on a clean turn boundary for the next turn.
185
- *
186
- * The `tool_call_id` is currently dropped by the wire format —
187
- * Gemma4's chat template identifies tool responses positionally,
188
- * not via an explicit id. Callers may still log it for their own
189
- * bookkeeping.
190
- *
191
- * Requires a live session started via `chatSessionStart`.
192
- */
193
- chatSessionContinueTool(
194
- toolCallId: string,
195
- content: string,
196
- config?: ChatConfig | undefined | null,
197
- ): Promise<ChatResult>;
210
+ chatSessionContinue(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
211
+ /**
212
+ * Continue an existing chat session from a complete
213
+ * structured conversation ending in a tool-role message.
214
+ */
215
+ chatSessionContinueTool(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
198
216
  /** Streaming variant of `chatSessionStart`. */
199
217
  chatStreamSessionStart(
200
218
  messages: ChatMessage[],
@@ -203,15 +221,13 @@ export declare class Gemma4Model {
203
221
  ): Promise<ChatStreamHandle>;
204
222
  /** Streaming variant of `chatSessionContinue`. */
205
223
  chatStreamSessionContinue(
206
- userMessage: string,
207
- images: Uint8Array[] | null | undefined,
224
+ messages: ChatMessage[],
208
225
  config: ChatConfig | null | undefined,
209
226
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
210
227
  ): Promise<ChatStreamHandle>;
211
228
  /** Streaming variant of `chatSessionContinueTool`. */
212
229
  chatStreamSessionContinueTool(
213
- toolCallId: string,
214
- content: string,
230
+ messages: ChatMessage[],
215
231
  config: ChatConfig | null | undefined,
216
232
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
217
233
  ): Promise<ChatStreamHandle>;
@@ -471,69 +487,53 @@ export declare class Lfm2Model {
471
487
  /** Load an LFM2 model from a directory containing safetensors and config.json. */
472
488
  static load(modelPath: string): Promise<Lfm2Model>;
473
489
  /**
474
- * Reset all caches and clear cached token history. Exposed so
475
- * tests and session-management code can start from a known clean
476
- * state between turns.
490
+ * Whether the block-paged KV cache adapter is active on this model
491
+ * instance.
492
+ *
493
+ * `true` iff `Lfm2Inner::paged_adapter` was successfully constructed
494
+ * at load time (driven by `Lfm2Config::use_block_paged_cache`,
495
+ * defaulting to `true` after paged-vs-flat parity verification).
496
+ * LFM2 is hybrid (10 conv + 6 full-attention layers); only the
497
+ * full-attention layers route through the adapter, conv layers stay
498
+ * on flat `Lfm2LayerCache::Conv` regardless. When `true`, the native
499
+ * cache reuses SYS blocks across `chatSessionStart` calls via
500
+ * content-addressing, so the JS-side warm slot in
501
+ * `SessionRegistry.getOrCreateWarmAny` is redundant and the
502
+ * `/v1/messages` server endpoint allocates a fresh `ChatSession` per
503
+ * request.
504
+ */
505
+ hasBlockPagedCache(): boolean;
506
+ /** Get the model configuration. */
507
+ getConfig(): Lfm2Config;
508
+ /** Estimated number of model parameters. */
509
+ numParameters(): number;
510
+ /**
511
+ * Reset all caches and clear cached token history. Exposed
512
+ * so tests and session-management code can start from a
513
+ * known clean state between turns.
477
514
  */
478
515
  resetCaches(): void;
479
516
  /**
480
517
  * Start a new chat session.
481
518
  *
482
- * Runs the full jinja chat template once, decodes until
483
- * `<|im_end|>`, and leaves the KV/conv caches on a clean ChatML
484
- * boundary so subsequent `chatSessionContinue` /
485
- * `chatSessionContinueTool` calls can append a raw delta on top
486
- * without re-rendering the chat template.
487
- *
488
- * Requires `config.reuse_cache` to be enabled (the default).
519
+ * Renders the complete conversation through the loaded chat
520
+ * template, decodes until the family's session stop token, and
521
+ * preserves the resulting KV state for exact-prefix reuse.
489
522
  */
490
523
  chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
491
524
  /**
492
- * Continue an existing chat session with a new user message.
493
- *
494
- * Appends a raw ChatML user/assistant delta to the session's
495
- * cached KV/conv state, then decodes the assistant reply. Stops
496
- * on `<|im_end|>` so the cache remains on a clean boundary for
497
- * the next turn.
498
- *
499
- * Requires a live session started via `chatSessionStart`. Errors
500
- * if the session is empty, carries image state, or if
501
- * `config.reuse_cache` is explicitly set to `false`.
502
- *
503
- * LFM2 is text-only; `images` is an opt-in guard parameter: when
504
- * non-empty the native side returns an error whose message begins
505
- * with `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
506
- * `ChatSession` layer can catch the prefix and route
507
- * image-changes back through a fresh `chatSessionStart`
508
- * uniformly across all model backends.
525
+ * Continue an existing chat session from the complete
526
+ * structured conversation. The loaded model template is the
527
+ * sole authority for the rendered suffix; native cache reuse
528
+ * occurs only after the completed structured history is verified
529
+ * against the saved token history.
509
530
  */
510
- chatSessionContinue(
511
- userMessage: string,
512
- images: Uint8Array[] | null | undefined,
513
- config: ChatConfig | null | undefined,
514
- ): Promise<ChatResult>;
531
+ chatSessionContinue(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
515
532
  /**
516
- * Continue an existing chat session with a tool-result turn.
517
- *
518
- * Builds an LFM2-format tool delta (`<|im_start|>tool
519
- {content}
520
- * <|im_end|>`) from `content` and prefills it on top of the live
521
- * session caches, then decodes the assistant reply. Stops on
522
- * `<|im_end|>` so the cache stays on a clean boundary for the
523
- * next turn.
524
- *
525
- * The `tool_call_id` is currently dropped by the wire format —
526
- * LFM2's chat template identifies tool responses positionally,
527
- * not via an explicit id. Callers may still log it for their own
528
- * bookkeeping.
529
- *
530
- * Requires a live session started via `chatSessionStart`.
531
- */
532
- chatSessionContinueTool(
533
- toolCallId: string,
534
- content: string,
535
- config?: ChatConfig | undefined | null,
536
- ): Promise<ChatResult>;
533
+ * Continue an existing chat session from a complete
534
+ * structured conversation ending in a tool-role message.
535
+ */
536
+ chatSessionContinueTool(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
537
537
  /** Streaming variant of `chatSessionStart`. */
538
538
  chatStreamSessionStart(
539
539
  messages: ChatMessage[],
@@ -542,22 +542,16 @@ export declare class Lfm2Model {
542
542
  ): Promise<ChatStreamHandle>;
543
543
  /** Streaming variant of `chatSessionContinue`. */
544
544
  chatStreamSessionContinue(
545
- userMessage: string,
546
- images: Uint8Array[] | null | undefined,
545
+ messages: ChatMessage[],
547
546
  config: ChatConfig | null,
548
547
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
549
548
  ): Promise<ChatStreamHandle>;
550
549
  /** Streaming variant of `chatSessionContinueTool`. */
551
550
  chatStreamSessionContinueTool(
552
- toolCallId: string,
553
- content: string,
551
+ messages: ChatMessage[],
554
552
  config: ChatConfig | null,
555
553
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
556
554
  ): Promise<ChatStreamHandle>;
557
- /** Get the model configuration. */
558
- getConfig(): Lfm2Config;
559
- /** Estimated number of model parameters. */
560
- numParameters(): number;
561
555
  }
562
556
 
563
557
  export declare class MxArray {
@@ -681,9 +675,20 @@ export declare class MxArray {
681
675
  divScalar(value: number): MxArray;
682
676
  matmul(other: MxArray): MxArray;
683
677
  /**
684
- * Fused matrix multiply-add: D = beta * C + alpha * (self @ B)
685
- * where self is A. More efficient than separate matmul and add operations.
686
- * Default: alpha=1.0, beta=1.0, giving D = C + (self @ B)
678
+ * Matrix multiply-add: D = beta * C + alpha * (self @ B), where self is A.
679
+ * Default: alpha=1.0, beta=1.0, giving D = C + (self @ B).
680
+ *
681
+ * Computed explicitly (matmul, optional alpha scale, then add beta*C)
682
+ * rather than via the fused `mlx::core::addmm` primitive. The fused
683
+ * primitive is correct on a well-formed metallib, but this project's local
684
+ * release build is known to non-deterministically miscompile fused GEMM
685
+ * kernels (see the metallib-corruption notes), which manifested as the
686
+ * fused addmm dropping `beta*C` and corrupting every biased linear — most
687
+ * visibly the vision tower (`qkv`, `proj`, `fc1`, `fc2`, merger all carry a
688
+ * bias; bias-free LM/MoE linears pass a zero `C` and were unaffected). The
689
+ * explicit form keeps the `C` term robust to that build hazard; the
690
+ * `nn::linear` unit tests assert it applies a non-zero `C`, so they double
691
+ * as a canary if a future build corrupts the matmul kernel too.
687
692
  */
688
693
  addmm(c: MxArray, b: MxArray, alpha?: number | undefined | null, beta?: number | undefined | null): MxArray;
689
694
  abs(): MxArray;
@@ -941,23 +946,43 @@ export declare class OutputStore {
941
946
  }
942
947
 
943
948
  /**
944
- * Opaque handle to KV cache state from a chat-session turn.
945
- *
946
- * Pass this back via `model.setCache(cache)` before the next
947
- * chat-session call to enable incremental prefill — only new tokens
948
- * since the last turn are processed, avoiding redundant computation.
949
+ * NAPI-exported view of [`PrivacyFilterModel`].
949
950
  *
950
- * Created internally by the model during chat-session turns.
951
- * Extract via `model.takeCache()`, restore via `model.setCache(cache)`.
951
+ * Construct via [`PrivacyFilterModelJs::load`] and run end-to-end
952
+ * classification via [`PrivacyFilterModelJs::classify`].
952
953
  */
953
- export declare class PromptCache {
954
- /** Number of tokens stored in this cache. */
955
- get tokenCount(): number;
956
- /** Whether this cache has been consumed (caches moved out). */
957
- get isEmpty(): boolean;
958
- /** Release GPU memory held by this cache. */
959
- dispose(): void;
954
+ export declare class PrivacyFilterModel {
955
+ /**
956
+ * Load a privacy-filter checkpoint from a directory.
957
+ *
958
+ * The directory must contain `config.json`, `model.safetensors`,
959
+ * `tokenizer.json`, and optionally `viterbi_calibration.json` and
960
+ * `tokenizer_config.json`. Synchronous to match the loader pattern
961
+ * used by every other model in this crate (e.g. `TextDetModel`).
962
+ */
963
+ static load(modelPath: string): PrivacyFilterModel;
964
+ /**
965
+ * Classify `text` and return detected PII entities (and optionally
966
+ * per-token tags).
967
+ *
968
+ * Pipeline:
969
+ * 1. Tokenize the text with byte offsets, no special tokens.
970
+ * 2. Run the forward pass to get `[1, T, 33]` logits.
971
+ * 3. Compute softmax (for per-tag confidences) and log-softmax (for
972
+ * Viterbi emissions) over the class axis.
973
+ * 4. Build the transition matrix from the default calibration
974
+ * merged with any per-call overrides.
975
+ * 5. Viterbi-decode using log-softmax emissions to get the BIOES
976
+ * tag sequence.
977
+ * 6. For each token, take the softmax probability of the
978
+ * Viterbi-emitted tag (so per-token `tag` and `score` share
979
+ * decoders), then walk the tags + offsets + those probabilities
980
+ * to extract coherent spans whose mean probability clears
981
+ * `threshold`.
982
+ */
983
+ classify(text: string, opts?: PrivacyClassifyOptions | undefined | null): PrivacyClassifyResult;
960
984
  }
985
+ export type PrivacyFilterModelJs = PrivacyFilterModel;
961
986
 
962
987
  /**
963
988
  * Qianfan-OCR Vision-Language Model (InternVL architecture).
@@ -973,8 +998,13 @@ export declare class QianfanOCRModel {
973
998
  * Create a new QianfanOCRModel from config (uninitialized, no weights).
974
999
  *
975
1000
  * This constructor path does not spawn a model thread — the returned
976
- * instance is only useful for config inspection. Call
977
- * [`QianfanOCRModel::load`] to actually run inference.
1001
+ * instance is only useful for `is_initialized` queries until
1002
+ * [`QianfanOCRModel::load`] is called to actually run inference. The
1003
+ * `config` argument is accepted to preserve the `new
1004
+ * QianfanOCRModel(config)` JS surface; the value is discarded because
1005
+ * nothing on the uninitialized path consults it (any future config
1006
+ * getter would forward to the inner thread state populated by
1007
+ * `load()`).
978
1008
  */
979
1009
  constructor(config: QianfanOcrConfig);
980
1010
  /** Returns true if weights have been loaded via `load()`. */
@@ -1004,11 +1034,9 @@ export declare class QianfanOCRModel {
1004
1034
  /**
1005
1035
  * Start a new chat session.
1006
1036
  *
1007
- * Runs the full chat template once, decodes until `<|im_end|>`,
1008
- * and leaves the KV caches on a clean turn boundary so subsequent
1009
- * `chatSessionContinue` / `chatSessionContinueTool` calls can
1010
- * append a raw ChatML delta on top without re-rendering the chat
1011
- * template.
1037
+ * Renders the complete structured conversation through the checkpoint
1038
+ * template, decodes until `<|im_end|>`, and preserves KV state for an
1039
+ * exact-prefix check against the next complete template render.
1012
1040
  *
1013
1041
  * Qianfan-OCR is always a VLM (InternViT + Qwen3 language model), so
1014
1042
  * this entry point accepts images in `messages` without the text-only
@@ -1016,62 +1044,31 @@ export declare class QianfanOCRModel {
1016
1044
  */
1017
1045
  chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1018
1046
  /**
1019
- * Continue an existing chat session with a new user message.
1020
- *
1021
- * Appends a raw ChatML user/assistant delta to the session's cached
1022
- * KV state, then decodes the model reply. Stops on `<|im_end|>` so
1023
- * the cache remains on a clean turn boundary for the next turn.
1024
- *
1025
- * Requires a live session started via `chatSessionStart`. Errors
1026
- * if the session is empty or if `config.reuse_cache` is
1027
- * explicitly set to `false`.
1028
- *
1029
- * `images` is an opt-in guard parameter: when non-empty the native
1030
- * side returns an error whose message begins with
1031
- * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
1032
- * `ChatSession` layer can catch the prefix and route image-changes
1033
- * back through a fresh `chatSessionStart` uniformly across all
1034
- * model backends. Qianfan-OCR is a VLM but the continue path cannot
1035
- * splice new vision features into a live KV cache — image changes
1036
- * always require a fresh session start.
1047
+ * Continue from the caller's complete conversation history. The
1048
+ * checkpoint's chat template is rendered again and exact prefix matching
1049
+ * decides whether the live cache can be reused.
1037
1050
  */
1038
- chatSessionContinue(
1039
- userMessage: string,
1040
- images: Uint8Array[] | null | undefined,
1041
- config: ChatConfig | null | undefined,
1042
- ): Promise<ChatResult>;
1051
+ chatSessionContinue(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1043
1052
  /**
1044
- * Continue an existing chat session with a tool-result turn.
1045
- *
1046
- * Builds a ChatML `<tool_response>` delta from `tool_call_id` and
1047
- * `content` and prefills it on top of the live session caches, then
1048
- * decodes the model reply. Stops on `<|im_end|>` so the cache stays
1049
- * on a clean turn boundary for the next turn.
1050
- *
1051
- * Requires a live session started via `chatSessionStart`.
1053
+ * Tool-result continuation over a full history. Tool representation is
1054
+ * owned entirely by the model-provided template.
1052
1055
  */
1053
- chatSessionContinueTool(
1054
- toolCallId: string,
1055
- content: string,
1056
- config?: ChatConfig | undefined | null,
1057
- ): Promise<ChatResult>;
1056
+ chatSessionContinueTool(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1058
1057
  /** Streaming variant of `chatSessionStart`. */
1059
1058
  chatStreamSessionStart(
1060
1059
  messages: ChatMessage[],
1061
1060
  config: ChatConfig | null | undefined,
1062
1061
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1063
1062
  ): Promise<ChatStreamHandle>;
1064
- /** Streaming variant of `chatSessionContinue`. */
1063
+ /** Streaming continuation over a complete conversation history. */
1065
1064
  chatStreamSessionContinue(
1066
- userMessage: string,
1067
- images: Uint8Array[] | null | undefined,
1065
+ messages: ChatMessage[],
1068
1066
  config: ChatConfig | null | undefined,
1069
1067
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1070
1068
  ): Promise<ChatStreamHandle>;
1071
- /** Streaming variant of `chatSessionContinueTool`. */
1069
+ /** Streaming tool-result continuation over a complete history. */
1072
1070
  chatStreamSessionContinueTool(
1073
- toolCallId: string,
1074
- content: string,
1071
+ messages: ChatMessage[],
1075
1072
  config: ChatConfig | null | undefined,
1076
1073
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1077
1074
  ): Promise<ChatStreamHandle>;
@@ -1085,27 +1082,55 @@ export declare class QianfanOCRModel {
1085
1082
  * routed through `TrainingDispatch` to the model thread.
1086
1083
  */
1087
1084
  export declare class Qwen35Model {
1088
- /** Initialize caches for incremental generation. */
1089
- initCaches(): void;
1090
- /** Reset all caches. */
1091
- resetCaches(): void;
1092
1085
  /**
1093
- * Take the KV cache from the model, returning a `PromptCache` handle.
1086
+ * Whether the block-paged KV cache adapter is active on this model
1087
+ * instance.
1094
1088
  *
1095
- * The cache is moved out of the model — calling `takeCache()` twice
1096
- * returns `null` the second time. Pass the cache back via `setCache()`
1097
- * before the next `chatSessionStart` / `chatSessionContinue` call for
1098
- * incremental prefill.
1089
+ * `true` iff `Qwen35Inner::paged_adapter` was successfully
1090
+ * constructed at load time (driven by
1091
+ * `Qwen3_5Config::use_block_paged_cache`, default-OFF for text-only
1092
+ * checkpoints because parity is pending real-weights validation, and
1093
+ * default-ON for VLM checkpoints). On VLM checkpoints dense image turns
1094
+ * ONLY run on the paged-vision core; a vision turn that reaches a None
1095
+ * adapter errors at dispatch. Surfaced through this NAPI method so
1096
+ * server endpoints can branch on it without round-tripping through
1097
+ * the model thread.
1098
+ */
1099
+ hasBlockPagedCache(): boolean;
1100
+ /**
1101
+ * Whether this checkpoint shipped an MTP head (module loaded by
1102
+ * `persistence::apply_weights_inner`). Snapshotted at load time from
1103
+ * `Qwen35Inner::has_mtp_weights()` so the TS `ChatSession` can
1104
+ * auto-default `enableMtp = true` for MTP-capable checkpoints without
1105
+ * dispatching a command into the model thread.
1106
+ *
1107
+ * Note: this only reports weight availability. Whether the
1108
+ * speculative-decode path actually runs on a given call also requires the
1109
+ * per-request `enableMtp` flag.
1110
+ */
1111
+ hasMtpWeights(): boolean;
1112
+ /**
1113
+ * Whether this loaded model instance can execute image-bearing turns.
1114
+ *
1115
+ * This is an authoritative load-time snapshot, not a `config.json`
1116
+ * family guess: it requires a loaded vision encoder, image processor,
1117
+ * and the block-paged KV adapter used by the dense vision path.
1118
+ */
1119
+ supportsImages(): boolean;
1120
+ /**
1121
+ * Synchronous snapshot used by higher layers to preflight rendered
1122
+ * prompts and clamp output before native cache allocation.
1099
1123
  */
1100
- takeCache(): PromptCache | null;
1124
+ contextLimits(): Qwen35ContextLimits;
1101
1125
  /**
1102
- * Restore a previously taken `PromptCache` into the model.
1126
+ * Compute the exact prompt length after Qwen image-placeholder expansion
1127
+ * without running the vision encoder or touching inference caches.
1103
1128
  *
1104
- * On the next `chatSessionStart` / `chatSessionContinue` call with
1105
- * `reuseCache: true`, the model will prefix-match the new tokens against
1106
- * the cache and only prefill the delta.
1129
+ * `prompt_tokens` is the already-rendered chat-template output. `messages`
1130
+ * supplies the complete image history so both fresh and leased-session
1131
+ * preflights account for every image in template order.
1107
1132
  */
1108
- setCache(cache: PromptCache): void;
1133
+ expandedPromptTokenCount(promptTokens: Uint32Array, messages: Array<ChatMessage>): Promise<number>;
1109
1134
  /**
1110
1135
  * Load a pretrained model from a directory.
1111
1136
  *
@@ -1118,130 +1143,62 @@ export declare class Qwen35Model {
1118
1143
  /** Generate text from a prompt token sequence. */
1119
1144
  generate(promptTokens: MxArray, config: Qwen35GenerationConfig): Promise<Qwen35GenerationResult>;
1120
1145
  /**
1121
- * Start a new chat session.
1146
+ * Get the number of parameters in the model.
1122
1147
  *
1123
- * Runs the full jinja chat template once and uses `<|im_end|>` as
1124
- * its stop token so the cached KV state ends on a clean ChatML
1125
- * boundary. Image support is conditional on the loaded
1126
- * checkpoint: a Qwen3.5-VL dense model loaded with vision weights
1127
- * accepts images in `messages` (the vision encoder handles
1128
- * prefill), while a plain text Qwen3.5 checkpoint rejects them
1129
- * with a runtime error. Subsequent turns in the same session MUST
1130
- * go through `chatSessionContinue` so the caller appends raw
1131
- * ChatML deltas on top of the live caches without rerunning the
1132
- * jinja template; a mid-session image change requires a fresh
1133
- * `chatSessionStart` call. The session is owned end-to-end by
1134
- * the `chatSession*` surface.
1135
- *
1136
- * This method is the production entry point used by the TypeScript
1137
- * `ChatSession` wrapper for turn 1 of a multi-round conversation.
1148
+ * Pure config computation no model-thread dispatch needed.
1138
1149
  */
1139
- chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1150
+ numParameters(): number;
1140
1151
  /**
1141
- * Continue an existing chat session with a new user message.
1142
- *
1143
- * Appends a raw ChatML user/assistant delta to the session's cached
1144
- * KV state, then decodes the assistant reply. Stops on `<|im_end|>`
1145
- * so the cache remains on a clean boundary for the next turn.
1146
- *
1147
- * Requires a live session started via `chatSessionStart`. Errors
1148
- * if the session is empty, carries image state, or if
1149
- * `config.reuse_cache` is explicitly set to `false`.
1152
+ * Save the model weights and configuration to a directory.
1150
1153
  *
1151
- * `images` is an opt-in guard parameter: when non-empty, the native
1152
- * side returns an error whose message begins with
1153
- * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
1154
- * `ChatSession` layer can catch the prefix and route image-changes
1155
- * back through a fresh `chatSessionStart`.
1154
+ * Dispatches to model thread.
1156
1155
  */
1157
- chatSessionContinue(
1158
- userMessage: string,
1159
- images: Uint8Array[] | null | undefined,
1160
- config: ChatConfig | null | undefined,
1161
- ): Promise<ChatResult>;
1156
+ saveModel(savePath: string): Promise<undefined>;
1162
1157
  /**
1163
- * Continue an existing chat session with a tool-result turn.
1164
- *
1165
- * Builds a ChatML `<tool_response>`-wrapped delta from `content` and
1166
- * prefills it on top of the live session caches, then decodes the
1167
- * assistant reply. Stops on `<|im_end|>` so the cache stays on a
1168
- * clean boundary for the next turn.
1169
- *
1170
- * The `tool_call_id` is currently dropped by the wire format —
1171
- * Qwen3.5's chat template identifies tool responses by position +
1172
- * wrapper tags, not an explicit id. Callers may still log it for
1173
- * their own bookkeeping.
1174
- *
1175
- * Requires a live session started via `chatSessionStart`.
1158
+ * Reset all caches and clear cached token history. Exposed
1159
+ * so tests and session-management code can start from a
1160
+ * known clean state between turns.
1176
1161
  */
1177
- chatSessionContinueTool(
1178
- toolCallId: string,
1179
- content: string,
1180
- config?: ChatConfig | undefined | null,
1181
- ): Promise<ChatResult>;
1162
+ resetCaches(): void;
1182
1163
  /**
1183
- * Streaming variant of `chatSessionStart`.
1164
+ * Start a new chat session.
1184
1165
  *
1185
- * Dispatches to the dedicated model thread. Behaviourally identical
1186
- * to `chatSessionStart` (resets caches, uses `<|im_end|>` as
1187
- * eos, inherits the same VLM-vs-text image-support contract) but
1188
- * streams token deltas through the JS callback instead of
1189
- * returning a `ChatResult`. Used by the TypeScript
1190
- * `ChatSession.sendStream()` for turn 1 of a multi-round streaming
1191
- * conversation.
1166
+ * Renders the complete conversation through the loaded chat
1167
+ * template, decodes until the family's session stop token, and
1168
+ * preserves the resulting KV state for exact-prefix reuse.
1169
+ */
1170
+ chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1171
+ /**
1172
+ * Continue an existing chat session from the complete
1173
+ * structured conversation. The loaded model template is the
1174
+ * sole authority for the rendered suffix; native cache reuse
1175
+ * occurs only after the completed structured history is verified
1176
+ * against the saved token history.
1177
+ */
1178
+ chatSessionContinue(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1179
+ /**
1180
+ * Continue an existing chat session from a complete
1181
+ * structured conversation ending in a tool-role message.
1192
1182
  */
1183
+ chatSessionContinueTool(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1184
+ /** Streaming variant of `chatSessionStart`. */
1193
1185
  chatStreamSessionStart(
1194
1186
  messages: ChatMessage[],
1195
1187
  config: ChatConfig | null,
1196
1188
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1197
1189
  ): Promise<ChatStreamHandle>;
1198
- /**
1199
- * Streaming variant of `chatSessionContinue`.
1200
- *
1201
- * Appends a ChatML user/assistant delta on top of the live session
1202
- * caches and streams the decoded reply. Requires a live session
1203
- * started via `chatStreamSessionStart` (or the non-streaming
1204
- * `chatSessionStart`). Used by the TypeScript
1205
- * `ChatSession.sendStream()` for turns 2..N of a multi-round
1206
- * streaming conversation.
1207
- *
1208
- * `images` is an opt-in guard parameter: when non-empty, the
1209
- * streaming path emits an error chunk whose message begins with
1210
- * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
1211
- * `ChatSession` layer can route image-changes through a fresh
1212
- * session start.
1213
- */
1190
+ /** Streaming variant of `chatSessionContinue`. */
1214
1191
  chatStreamSessionContinue(
1215
- userMessage: string,
1216
- images: Uint8Array[] | null | undefined,
1192
+ messages: ChatMessage[],
1217
1193
  config: ChatConfig | null,
1218
1194
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1219
1195
  ): Promise<ChatStreamHandle>;
1220
- /**
1221
- * Streaming variant of `chatSessionContinueTool`.
1222
- *
1223
- * Builds a ChatML tool-response delta on top of the live session
1224
- * caches and streams the decoded reply. Requires a live session
1225
- * started via `chatSessionStart` / `chatStreamSessionStart`.
1226
- */
1196
+ /** Streaming variant of `chatSessionContinueTool`. */
1227
1197
  chatStreamSessionContinueTool(
1228
- toolCallId: string,
1229
- content: string,
1198
+ messages: ChatMessage[],
1230
1199
  config: ChatConfig | null,
1231
1200
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1232
1201
  ): Promise<ChatStreamHandle>;
1233
- /**
1234
- * Get the number of parameters in the model.
1235
- *
1236
- * Pure config computation — no model-thread dispatch needed.
1237
- */
1238
- numParameters(): number;
1239
- /**
1240
- * Save the model weights and configuration to a directory.
1241
- *
1242
- * Dispatches to model thread.
1243
- */
1244
- saveModel(savePath: string): Promise<undefined>;
1245
1202
  }
1246
1203
  export type Qwen3_5Model = Qwen35Model;
1247
1204
 
@@ -1253,142 +1210,110 @@ export type Qwen3_5Model = Qwen35Model;
1253
1210
  * routed through `TrainingDispatch` to the model thread.
1254
1211
  */
1255
1212
  export declare class Qwen35MoeModel {
1256
- /** Initialize caches for incremental generation. */
1257
- initCaches(): void;
1258
- /** Reset all caches. */
1259
- resetCaches(): void;
1260
- /** Take the KV cache from the model, returning a `PromptCache` handle. */
1261
- takeCache(): PromptCache | null;
1262
- /** Restore a previously taken `PromptCache` into the model. */
1263
- setCache(cache: PromptCache): void;
1213
+ /**
1214
+ * Whether the block-paged KV cache adapter is active on this model
1215
+ * instance.
1216
+ *
1217
+ * `true` iff `Qwen35MoeInner::paged_adapter` was successfully
1218
+ * constructed at load time (driven by
1219
+ * `Qwen3_5MoeConfig::use_block_paged_cache`, currently default-OFF
1220
+ * because parity is pending real-weights validation). On VLM
1221
+ * checkpoints the adapter can still be active for text-only
1222
+ * inference; image-bearing chat turns are rejected at runtime by
1223
+ * the chat-entry sites. Surfaced through this NAPI method so
1224
+ * server endpoints can branch on it without round-tripping through
1225
+ * the model thread.
1226
+ */
1227
+ hasBlockPagedCache(): boolean;
1228
+ /**
1229
+ * Whether this checkpoint shipped an MTP head (module loaded by
1230
+ * `persistence::apply_weights_moe_inner`). Snapshotted at load time from
1231
+ * `Qwen35MoeInner::has_mtp_weights()` so the TS `ChatSession` can
1232
+ * auto-default `enableMtp = true` for MTP-capable checkpoints without
1233
+ * dispatching a command into the model thread. Mirrors
1234
+ * `Qwen3_5Model::has_mtp_weights`.
1235
+ *
1236
+ * Note: this only reports weight availability. Whether the
1237
+ * speculative-decode path actually runs on a given call also requires
1238
+ * the per-request `enableMtp` flag.
1239
+ */
1240
+ hasMtpWeights(): boolean;
1241
+ /**
1242
+ * Whether this loaded model instance can execute image-bearing turns.
1243
+ *
1244
+ * This is an authoritative load-time snapshot, not a model-family guess:
1245
+ * it requires the loaded vision encoder, image processor, and block-paged
1246
+ * KV adapter used by the MoE vision path.
1247
+ */
1248
+ supportsImages(): boolean;
1249
+ /** Synchronous active-context snapshot shared with the dense wrapper. */
1250
+ contextLimits(): Qwen35ContextLimits;
1251
+ /**
1252
+ * Exact, non-mutating Qwen image-placeholder expansion count for a fully
1253
+ * rendered prompt and complete message history.
1254
+ */
1255
+ expandedPromptTokenCount(promptTokens: Uint32Array, messages: Array<ChatMessage>): Promise<number>;
1264
1256
  /** Load a pretrained model from a directory. */
1265
1257
  static load(path: string): Promise<Qwen35MoeModel>;
1266
1258
  /** Generate text from a prompt token sequence. */
1267
1259
  generate(promptTokens: MxArray, config: Qwen35MoeGenerationConfig): Promise<Qwen35MoeGenerationResult>;
1268
1260
  /**
1269
- * Start a new chat session.
1270
- *
1271
- * Runs the full jinja chat template once, decodes until `<|im_end|>`,
1272
- * and leaves the KV caches on a clean ChatML boundary so subsequent
1273
- * `chatSessionContinue` / `chatSessionContinueTool` calls can
1274
- * append a raw delta on top without re-rendering the chat
1275
- * template.
1276
- *
1277
- * Image support is conditional on the loaded checkpoint: a
1278
- * Qwen3.5-VL MoE model loaded with vision weights accepts images
1279
- * in `messages` (the vision encoder handles prefill), while a
1280
- * plain text Qwen3.5 MoE checkpoint rejects them with a runtime
1281
- * error. A mid-session image change requires a fresh
1282
- * `chatSessionStart` call.
1261
+ * Get the number of parameters in the model.
1283
1262
  *
1284
- * Requires `config.reuse_cache` to be enabled (the default).
1263
+ * Pure config computation -- no model-thread dispatch needed.
1285
1264
  */
1286
- chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1265
+ numParameters(): number;
1287
1266
  /**
1288
- * Continue an existing chat session with a new user message.
1289
- *
1290
- * Appends a raw ChatML user/assistant delta to the session's cached
1291
- * KV state, then decodes the assistant reply. Stops on `<|im_end|>`
1292
- * so the cache remains on a clean boundary for the next turn.
1293
- *
1294
- * Requires a live session started via `chatSessionStart`.
1295
- * Errors if the session is empty, carries image state, or if
1296
- * `config.reuse_cache` is explicitly set to `false`.
1267
+ * Save the model weights and configuration to a directory.
1297
1268
  *
1298
- * `images` is an opt-in guard parameter: when non-empty, the native
1299
- * side returns an error whose message begins with
1300
- * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
1301
- * `ChatSession` layer can catch the prefix and route image-changes
1302
- * back through a fresh `chatSessionStart`.
1269
+ * Dispatches to model thread.
1303
1270
  */
1304
- chatSessionContinue(
1305
- userMessage: string,
1306
- images: Uint8Array[] | null | undefined,
1307
- config: ChatConfig | null | undefined,
1308
- ): Promise<ChatResult>;
1271
+ saveModel(savePath: string): Promise<undefined>;
1309
1272
  /**
1310
- * Continue an existing chat session with a tool-result turn.
1311
- *
1312
- * Builds a ChatML `<tool_response>`-wrapped delta from `content` and
1313
- * prefills it on top of the live session caches, then decodes the
1314
- * assistant reply. Stops on `<|im_end|>` so the cache stays on a
1315
- * clean boundary for the next turn.
1316
- *
1317
- * The `tool_call_id` is currently dropped by the wire format —
1318
- * Qwen3.5's chat template identifies tool responses by position +
1319
- * wrapper tags, not an explicit id. Callers may still log it for
1320
- * their own bookkeeping.
1321
- *
1322
- * Requires a live session started via `chatSessionStart`.
1273
+ * Reset all caches and clear cached token history. Exposed
1274
+ * so tests and session-management code can start from a
1275
+ * known clean state between turns.
1323
1276
  */
1324
- chatSessionContinueTool(
1325
- toolCallId: string,
1326
- content: string,
1327
- config?: ChatConfig | undefined | null,
1328
- ): Promise<ChatResult>;
1277
+ resetCaches(): void;
1329
1278
  /**
1330
- * Streaming variant of `chatSessionStart`.
1279
+ * Start a new chat session.
1331
1280
  *
1332
- * Dispatches to the dedicated model thread. Behaviourally identical
1333
- * to `chatSessionStart` (resets caches, uses `<|im_end|>` as
1334
- * eos, inherits the same VLM-vs-text image-support contract) but
1335
- * streams token deltas through the JS callback instead of
1336
- * returning a `ChatResult`. Used by the TypeScript
1337
- * `ChatSession.sendStream()` for turn 1 of a multi-round streaming
1338
- * conversation.
1281
+ * Renders the complete conversation through the loaded chat
1282
+ * template, decodes until the family's session stop token, and
1283
+ * preserves the resulting KV state for exact-prefix reuse.
1284
+ */
1285
+ chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1286
+ /**
1287
+ * Continue an existing chat session from the complete
1288
+ * structured conversation. The loaded model template is the
1289
+ * sole authority for the rendered suffix; native cache reuse
1290
+ * occurs only after the completed structured history is verified
1291
+ * against the saved token history.
1292
+ */
1293
+ chatSessionContinue(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1294
+ /**
1295
+ * Continue an existing chat session from a complete
1296
+ * structured conversation ending in a tool-role message.
1339
1297
  */
1298
+ chatSessionContinueTool(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1299
+ /** Streaming variant of `chatSessionStart`. */
1340
1300
  chatStreamSessionStart(
1341
1301
  messages: ChatMessage[],
1342
1302
  config: ChatConfig | null,
1343
1303
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1344
1304
  ): Promise<ChatStreamHandle>;
1345
- /**
1346
- * Streaming variant of `chatSessionContinue`.
1347
- *
1348
- * Appends a ChatML user/assistant delta on top of the live session
1349
- * caches and streams the decoded reply. Requires a live session
1350
- * started via `chatStreamSessionStart` (or the non-streaming
1351
- * `chatSessionStart`). Used by the TypeScript
1352
- * `ChatSession.sendStream()` for turns 2..N of a multi-round
1353
- * streaming conversation.
1354
- *
1355
- * `images` is an opt-in guard parameter: when non-empty, the
1356
- * streaming path emits an error chunk whose message begins with
1357
- * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
1358
- * `ChatSession` layer can route image-changes through a fresh
1359
- * session start.
1360
- */
1305
+ /** Streaming variant of `chatSessionContinue`. */
1361
1306
  chatStreamSessionContinue(
1362
- userMessage: string,
1363
- images: Uint8Array[] | null | undefined,
1307
+ messages: ChatMessage[],
1364
1308
  config: ChatConfig | null,
1365
1309
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1366
1310
  ): Promise<ChatStreamHandle>;
1367
- /**
1368
- * Streaming variant of `chatSessionContinueTool`.
1369
- *
1370
- * Builds a ChatML tool-response delta on top of the live session
1371
- * caches and streams the decoded reply. Requires a live session
1372
- * started via `chatSessionStart` / `chatStreamSessionStart`.
1373
- */
1311
+ /** Streaming variant of `chatSessionContinueTool`. */
1374
1312
  chatStreamSessionContinueTool(
1375
- toolCallId: string,
1376
- content: string,
1313
+ messages: ChatMessage[],
1377
1314
  config: ChatConfig | null,
1378
1315
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1379
1316
  ): Promise<ChatStreamHandle>;
1380
- /**
1381
- * Get the number of parameters in the model.
1382
- *
1383
- * Pure config computation -- no model-thread dispatch needed.
1384
- */
1385
- numParameters(): number;
1386
- /**
1387
- * Save the model weights and configuration to a directory.
1388
- *
1389
- * Dispatches to model thread.
1390
- */
1391
- saveModel(savePath: string): Promise<undefined>;
1392
1317
  }
1393
1318
  export type Qwen3_5MoeModel = Qwen35MoeModel;
1394
1319
 
@@ -1400,117 +1325,21 @@ export type Qwen3_5MoeModel = Qwen35MoeModel;
1400
1325
  */
1401
1326
  export declare class Qwen3Model {
1402
1327
  /**
1403
- * Reset the KV cache used for cache reuse across chat-session turns.
1404
- * Call this when starting a new conversation to ensure a full prefill.
1405
- */
1406
- resetCache(): void;
1407
- /**
1408
- * Initialize KV caches for incremental generation
1409
- *
1410
- * Creates one KV cache per transformer layer. Call this before starting generation.
1411
- */
1412
- initKvCaches(): void;
1413
- /**
1414
- * Reset all KV caches
1415
- *
1416
- * Clears cached key-value states. Call this between different generation sequences.
1417
- */
1418
- resetKvCaches(): void;
1419
- /**
1420
- * Get paged attention memory statistics (if enabled)
1421
- *
1422
- * Returns memory usage statistics for the paged KV cache.
1423
- */
1424
- pagedCacheStats(): PagedCacheStats | null;
1425
- /**
1426
- * Get scheduler statistics (if paged attention is enabled)
1427
- *
1428
- * Returns the number of waiting, running, and completed sequences.
1429
- */
1430
- schedulerStats(): SchedulerStatsNapi | null;
1431
- /**
1432
- * Forward pass with paged attention for memory-efficient inference.
1433
- *
1434
- * This method uses block-based KV cache management via Metal kernels for:
1435
- * - Variable-length sequences with efficient memory usage
1436
- * - Continuous batching with dynamic batch composition
1437
- * - Long context support beyond GPU memory limits
1438
- *
1439
- * # Arguments
1440
- * * `input_ids` - Token IDs, shape: [num_seqs, 1] for decode
1441
- * * `slot_mapping` - Slot indices for cache updates, shape: [num_seqs]
1442
- * * `seq_ids` - Sequence IDs in the batch (for looking up block tables/context lens)
1443
- * * `positions` - Token positions for RoPE, shape: [num_seqs] (per-sequence positions)
1444
- *
1445
- * # Returns
1446
- * * Logits, shape: [num_seqs, 1, vocab_size] for decode
1447
- */
1448
- forwardPaged(inputIds: MxArray, slotMapping: MxArray, seqIds: Array<number>, positions: MxArray): MxArray;
1449
- /**
1450
- * Prefill a sequence using standard attention and write K/V to paged cache.
1451
- *
1452
- * This method should be called before `step_paged_generation()` for each
1453
- * new prompt. It runs the full forward pass using standard attention
1454
- * (which is faster for long sequences), then writes the K/V cache to
1455
- * the paged cache for subsequent decode steps.
1456
- *
1457
- * # Arguments
1458
- * * `prompt_tokens` - Token IDs for the prompt (as u32 array)
1459
- * * `seq_id` - Sequence ID (obtained from scheduler)
1460
- *
1461
- * # Returns
1462
- * * Logits for the last token, shape: [1, vocab_size]
1463
- */
1464
- prefillPaged(promptTokens: Array<number>, seqId: number): MxArray;
1465
- /**
1466
- * Add a request to the paged attention scheduler.
1467
- *
1468
- * The scheduler queues requests and allocates blocks for KV cache.
1469
- * Use `step_paged_generation()` to process the scheduled batch.
1470
- *
1471
- * Note: The actual sequence ID is assigned during scheduling, not when the
1472
- * request is added. Use the `request_id` to track your requests through
1473
- * the generation process.
1474
- *
1475
- * # Arguments
1476
- * * `request_id` - Unique identifier for the request (returned in outputs)
1477
- * * `prompt_tokens` - Token IDs for the prompt
1478
- * * `max_new_tokens` - Maximum new tokens to generate
1479
- * * `priority` - Optional priority (higher = scheduled first)
1480
- *
1481
- * # Returns
1482
- * * Number of pending requests in the queue
1483
- */
1484
- addPagedRequest(
1485
- requestId: string,
1486
- promptTokens: Array<number>,
1487
- maxNewTokens: number,
1488
- priority?: number | undefined | null,
1489
- ): number;
1490
- /**
1491
- * Schedule and execute one step of paged generation.
1492
- *
1493
- * This method:
1494
- * 1. Schedules the next batch of sequences
1495
- * 2. Runs forward pass with paged attention
1496
- * 3. Samples next tokens
1497
- * 4. Returns the generated tokens for each sequence
1498
- *
1499
- * # Arguments
1500
- * * `config` - Generation configuration (temperature, top_k, etc.)
1501
- *
1502
- * # Returns
1503
- * * `PagedGenerationStep` with token outputs for each sequence
1504
- */
1505
- stepPagedGeneration(config?: GenerationConfig | undefined | null): PagedGenerationStep | null;
1506
- /**
1507
- * Get completed sequences from the scheduler.
1508
- *
1509
- * Call this after `step_paged_generation()` returns outputs with `is_finished: true`.
1510
- */
1511
- getCompletedSequences(): Array<PagedCompletedSequence>;
1512
- /** Check if the scheduler has pending work. */
1513
- hasPagedWork(): boolean;
1328
+ * Whether the block-paged KV cache adapter is active on this model
1329
+ * instance.
1330
+ *
1331
+ * `true` iff `Qwen3Inner::paged_adapter` was successfully constructed
1332
+ * at load time (driven by `Qwen3Config::use_block_paged_cache`,
1333
+ * defaulting to `true` for Qwen3 since paged-vs-flat parity has been
1334
+ * verified). When `true`, the native cache reuses SYS blocks across
1335
+ * `chatSessionStart` calls via content-addressing in
1336
+ * `BlockAllocator`'s prefix-hash table — the JS-side warm slot in
1337
+ * `SessionRegistry.getOrCreateWarmAny` becomes redundant and the
1338
+ * `/v1/messages` server endpoint allocates a fresh `ChatSession` per
1339
+ * request. See `packages/server/src/endpoints/messages.ts` for the
1340
+ * runtime-routing decision.
1341
+ */
1342
+ hasBlockPagedCache(): boolean;
1514
1343
  /** Get model configuration */
1515
1344
  getConfig(): Qwen3Config;
1516
1345
  /**
@@ -1544,83 +1373,6 @@ export declare class Qwen3Model {
1544
1373
  * ```
1545
1374
  */
1546
1375
  generate(messages: Array<ChatMessage>, config?: GenerationConfig | undefined | null): Promise<GenerationResult>;
1547
- /**
1548
- * Reset all caches and clear cached token history. Exposed so
1549
- * tests and session-management code can start from a known clean
1550
- * state between turns.
1551
- */
1552
- resetCaches(): void;
1553
- /**
1554
- * Start a new chat session.
1555
- *
1556
- * Runs the full jinja chat template once, decodes until `<|im_end|>`,
1557
- * and leaves the KV caches on a clean ChatML boundary so subsequent
1558
- * `chatSessionContinue` / `chatSessionContinueTool` calls can
1559
- * append a raw delta on top without re-rendering the chat
1560
- * template.
1561
- *
1562
- * Requires `config.reuse_cache` to be enabled (the default).
1563
- */
1564
- chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1565
- /**
1566
- * Continue an existing chat session with a new user message.
1567
- *
1568
- * Appends a raw ChatML user/assistant delta to the session's
1569
- * cached KV state, then decodes the assistant reply. Stops on
1570
- * `<|im_end|>` so the cache remains on a clean boundary for the
1571
- * next turn.
1572
- *
1573
- * Requires a live session started via `chatSessionStart`. Errors
1574
- * if the session is empty, carries image state, or if
1575
- * `config.reuse_cache` is explicitly set to `false`.
1576
- *
1577
- * Qwen3 legacy is text-only; `images` is an opt-in guard parameter:
1578
- * when non-empty the native side returns an error whose message
1579
- * begins with `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the
1580
- * TypeScript `ChatSession` layer can catch the prefix and route
1581
- * image-changes back through a fresh `chatSessionStart` uniformly
1582
- * across all model backends.
1583
- */
1584
- chatSessionContinue(
1585
- userMessage: string,
1586
- images: Uint8Array[] | null | undefined,
1587
- config: ChatConfig | null | undefined,
1588
- ): Promise<ChatResult>;
1589
- /**
1590
- * Continue an existing chat session with a tool-result turn.
1591
- *
1592
- * Builds a Qwen3.5-style `<tool_response>`-wrapped user-role delta
1593
- * from `content` and prefills it on top of the live session
1594
- * caches, then decodes the assistant reply. Stops on `<|im_end|>`
1595
- * so the cache stays on a clean boundary for the next turn.
1596
- *
1597
- * Requires a live session started via `chatSessionStart`.
1598
- */
1599
- chatSessionContinueTool(
1600
- toolCallId: string,
1601
- content: string,
1602
- config?: ChatConfig | undefined | null,
1603
- ): Promise<ChatResult>;
1604
- /** Streaming variant of `chatSessionStart`. */
1605
- chatStreamSessionStart(
1606
- messages: ChatMessage[],
1607
- config: ChatConfig | null,
1608
- callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1609
- ): Promise<ChatStreamHandle>;
1610
- /** Streaming variant of `chatSessionContinue`. */
1611
- chatStreamSessionContinue(
1612
- userMessage: string,
1613
- images: Uint8Array[] | null | undefined,
1614
- config: ChatConfig | null,
1615
- callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1616
- ): Promise<ChatStreamHandle>;
1617
- /** Streaming variant of `chatSessionContinueTool`. */
1618
- chatStreamSessionContinueTool(
1619
- toolCallId: string,
1620
- content: string,
1621
- config: ChatConfig | null,
1622
- callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1623
- ): Promise<ChatStreamHandle>;
1624
1376
  /**
1625
1377
  * Generate multiple completions for multiple prompts in batch
1626
1378
  *
@@ -1660,20 +1412,6 @@ export declare class Qwen3Model {
1660
1412
  groupSize: number,
1661
1413
  config?: GenerationConfig | undefined | null,
1662
1414
  ): Promise<BatchGenerationResult>;
1663
- /**
1664
- * Decode token IDs to text using the internal tokenizer
1665
- *
1666
- * Helper method for decoding generated tokens. The model must have been loaded
1667
- * via load() to have a tokenizer available.
1668
- *
1669
- * # Arguments
1670
- * * `token_ids` - Token IDs to decode as Uint32Array
1671
- * * `skip_special_tokens` - Whether to skip special tokens (default: true)
1672
- *
1673
- * # Returns
1674
- * * Decoded text string
1675
- */
1676
- decode(tokenIds: Uint32Array, skipSpecialTokens?: boolean | undefined | null): Promise<string>;
1677
1415
  /**
1678
1416
  * Apply chat template and encode to token IDs
1679
1417
  *
@@ -1695,6 +1433,51 @@ export declare class Qwen3Model {
1695
1433
  tools?: Array<ToolDefinition> | undefined | null,
1696
1434
  enableThinking?: boolean | undefined | null,
1697
1435
  ): Promise<Uint32Array>;
1436
+ /**
1437
+ * Reset all caches and clear cached token history. Exposed
1438
+ * so tests and session-management code can start from a
1439
+ * known clean state between turns.
1440
+ */
1441
+ resetCaches(): void;
1442
+ /**
1443
+ * Start a new chat session.
1444
+ *
1445
+ * Renders the complete conversation through the loaded chat
1446
+ * template, decodes until the family's session stop token, and
1447
+ * preserves the resulting KV state for exact-prefix reuse.
1448
+ */
1449
+ chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1450
+ /**
1451
+ * Continue an existing chat session from the complete
1452
+ * structured conversation. The loaded model template is the
1453
+ * sole authority for the rendered suffix; native cache reuse
1454
+ * occurs only after the completed structured history is verified
1455
+ * against the saved token history.
1456
+ */
1457
+ chatSessionContinue(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1458
+ /**
1459
+ * Continue an existing chat session from a complete
1460
+ * structured conversation ending in a tool-role message.
1461
+ */
1462
+ chatSessionContinueTool(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1463
+ /** Streaming variant of `chatSessionStart`. */
1464
+ chatStreamSessionStart(
1465
+ messages: ChatMessage[],
1466
+ config: ChatConfig | null,
1467
+ callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1468
+ ): Promise<ChatStreamHandle>;
1469
+ /** Streaming variant of `chatSessionContinue`. */
1470
+ chatStreamSessionContinue(
1471
+ messages: ChatMessage[],
1472
+ config: ChatConfig | null,
1473
+ callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1474
+ ): Promise<ChatStreamHandle>;
1475
+ /** Streaming variant of `chatSessionContinueTool`. */
1476
+ chatStreamSessionContinueTool(
1477
+ messages: ChatMessage[],
1478
+ config: ChatConfig | null,
1479
+ callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1480
+ ): Promise<ChatStreamHandle>;
1698
1481
  /**
1699
1482
  * Load a pretrained model from disk
1700
1483
  *
@@ -1828,6 +1611,9 @@ export declare class Qwen3Tokenizer {
1828
1611
  * * `add_generation_prompt` - Whether to add assistant prompt at end (default: true)
1829
1612
  * * `tools` - Optional array of tool definitions for function calling
1830
1613
  * * `enable_thinking` - Optional flag to enable thinking mode (<think> tags)
1614
+ * * `content_order` - Optional structured multimodal content ordering
1615
+ * * `existing_image_placeholder` - Optional model marker that suppresses
1616
+ * synthetic image parts when it is already present in sanitized text
1831
1617
  *
1832
1618
  * # Returns
1833
1619
  * Encoded token IDs ready for model input
@@ -1853,6 +1639,8 @@ export declare class Qwen3Tokenizer {
1853
1639
  addGenerationPrompt?: boolean | undefined | null,
1854
1640
  tools?: Array<ToolDefinition> | undefined | null,
1855
1641
  enableThinking?: boolean | undefined | null,
1642
+ contentOrder?: MultimodalContentOrder | undefined | null,
1643
+ existingImagePlaceholder?: string | undefined | null,
1856
1644
  ): Promise<Uint32Array>;
1857
1645
  /** Get vocabulary size */
1858
1646
  vocabSize(): number;
@@ -2135,17 +1923,11 @@ export declare class TextRecModel {
2135
1923
  recognizeCrop(rgbData: Uint8Array, width: number, height: number): RecResult;
2136
1924
  }
2137
1925
 
2138
- /** Result from VLM chat */
2139
1926
  export declare class VlmChatResult {
2140
- /** Get the response text */
2141
1927
  get text(): string;
2142
- /** Get the generated tokens */
2143
1928
  get tokens(): MxArray;
2144
- /** Get the log probabilities */
2145
1929
  get logprobs(): MxArray;
2146
- /** Get the finish reason */
2147
1930
  get finishReason(): 'stop' | 'length' | 'repetition';
2148
- /** Get the number of tokens generated */
2149
1931
  get numTokens(): number;
2150
1932
  }
2151
1933
  export type VLMChatResult = VlmChatResult;
@@ -2167,10 +1949,6 @@ export declare class VLModel {
2167
1949
  * for loading a model from disk.
2168
1950
  */
2169
1951
  constructor(config: ModelConfig);
2170
- /** Set the tokenizer */
2171
- setTokenizer(tokenizer: Qwen3Tokenizer): void;
2172
- /** Check if tokenizer is available */
2173
- get hasTokenizer(): boolean;
2174
1952
  /**
2175
1953
  * Chat with the VLM model
2176
1954
  *
@@ -2211,80 +1989,6 @@ export declare class VLModel {
2211
1989
  * ```
2212
1990
  */
2213
1991
  ocr(imageData: Buffer, prompt?: string | undefined | null): Promise<string>;
2214
- /**
2215
- * Get input embeddings with vision features merged
2216
- *
2217
- * # Arguments
2218
- * * `input_ids` - Token IDs [batch, seq_len]
2219
- * * `pixel_values` - Optional image patches [batch, seq, channels, patch_h, patch_w]
2220
- * * `image_grid_thw` - Optional grid dimensions [num_images, 3]
2221
- *
2222
- * # Returns
2223
- * * Input embeddings with vision features inserted at image token positions
2224
- */
2225
- getInputEmbeddings(
2226
- inputIds: MxArray,
2227
- pixelValues?: MxArray | undefined | null,
2228
- imageGridThw?: MxArray | undefined | null,
2229
- ): MxArray;
2230
- /**
2231
- * Forward pass
2232
- *
2233
- * # Arguments
2234
- * * `input_ids` - Token IDs [batch, seq_len]
2235
- * * `pixel_values` - Optional image patches
2236
- * * `image_grid_thw` - Optional grid dimensions
2237
- * * `mask` - Optional attention mask
2238
- *
2239
- * # Returns
2240
- * * Logits [batch, seq_len, vocab_size]
2241
- */
2242
- forward(
2243
- inputIds: MxArray,
2244
- pixelValues?: MxArray | undefined | null,
2245
- imageGridThw?: MxArray | undefined | null,
2246
- mask?: MxArray | undefined | null,
2247
- ): MxArray;
2248
- /**
2249
- * Generate text tokens given input tokens and optional image
2250
- *
2251
- * Uses KV caching for efficient generation.
2252
- *
2253
- * # Arguments
2254
- * * `input_ids` - Input token IDs [1, seq_len]
2255
- * * `pixel_values` - Optional image patches [1, num_patches, C, H, W]
2256
- * * `image_grid_thw` - Optional grid dimensions [1, 3]
2257
- * * `config` - Generation configuration
2258
- *
2259
- * # Returns
2260
- * * GenerationResult with tokens, logprobs, and finish reason
2261
- */
2262
- generate(
2263
- inputIds: MxArray,
2264
- pixelValues?: MxArray | undefined | null,
2265
- imageGridThw?: MxArray | undefined | null,
2266
- config?: GenerationConfig | undefined | null,
2267
- ): Promise<GenerationResult>;
2268
- /**
2269
- * Batch OCR: extract text from multiple images simultaneously
2270
- *
2271
- * Processes N images with sequential prefill + batched decode for ~N× decode throughput.
2272
- *
2273
- * # Arguments
2274
- * * `images` - Encoded image buffers
2275
- * * `config` - Optional chat configuration (shared across all items)
2276
- *
2277
- * # Returns
2278
- * * Vec of extracted text strings, one per image
2279
- *
2280
- * # Example
2281
- * ```typescript
2282
- * import { readFileSync } from 'fs';
2283
- * const images = ['page1.jpg', 'page2.jpg'].map(p => readFileSync(p));
2284
- * const texts = await model.ocrBatch(images);
2285
- * ```
2286
- */
2287
- ocrBatch(images: Array<Buffer>, config?: VlmChatConfig | undefined | null): Promise<Array<string>>;
2288
1992
  /**
2289
1993
  * Batch chat: process multiple items simultaneously
2290
1994
  *
@@ -2323,25 +2027,6 @@ export declare class VLModel {
2323
2027
  * ```
2324
2028
  */
2325
2029
  static load(modelPath: string): Promise<VLModel>;
2326
- /**
2327
- * Load model configuration from disk without loading weights
2328
- *
2329
- * This is useful for inspecting model configuration before loading the full model.
2330
- *
2331
- * # Arguments
2332
- * * `model_path` - Path to the model directory containing config.json
2333
- *
2334
- * # Returns
2335
- * * ModelConfig with vision and text configuration
2336
- *
2337
- * # Example
2338
- * ```typescript
2339
- * import { VLModel } from '@mlx-node/vlm';
2340
- * const config = await VLModel.loadConfig('./models/paddleocr-vl');
2341
- * console.log(config.visionConfig.hiddenSize);
2342
- * ```
2343
- */
2344
- static loadConfig(modelPath: string): Promise<ModelConfig>;
2345
2030
  }
2346
2031
 
2347
2032
  /**
@@ -2392,8 +2077,68 @@ export declare const enum BuiltinRewardType {
2392
2077
  JsonSchema = 'JsonSchema',
2393
2078
  }
2394
2079
 
2080
+ /**
2081
+ * Data-free static FP8 activation-amax calibration over RAW-text PREFILL
2082
+ * (NVIDIA modelopt `MaxCalibrator` parity), end to end in native code.
2083
+ *
2084
+ * The nvidia recipe covers BOTH `qwen3_5` (dense) and `qwen3_5_moe` (MoE), so
2085
+ * this reads `<model_path>/config.json`'s `model_type` and dispatches to the
2086
+ * matching loader + `CalibratePrefillRaw` command (any other `model_type` is a
2087
+ * clear error). Both loaders are the SAME ones the inference session uses
2088
+ * ([`persistence::load_with_thread`]) — the model is only usable on its
2089
+ * dedicated model thread. Then:
2090
+ * 1. dispatches `{Qwen35Cmd,Qwen35MoeCmd}::CalibratePrefillRaw`, which on the
2091
+ * model thread SELF-ARMS that thread's thread-local
2092
+ * [`ActivationAmaxCollector`] flag (RAII, AFTER load so no load-time eval
2093
+ * is recorded), tokenizes each `text` WITHOUT the chat template, truncates
2094
+ * to `calib_seq` tokens, and runs PREFILL ONLY (no generation) so every
2095
+ * mxfp8 attn/GDN projection's activation tap fires over realistic raw-text
2096
+ * activations, resetting caches between rows, then disarms on exit;
2097
+ * 2. ONLY if the full loop succeeded — drains the per-tensor amax and
2098
+ * ATOMICALLY writes it into `<model_path>/config.json` (temp file +
2099
+ * `rename`).
2100
+ *
2101
+ * CONCURRENCY: the whole clear→prefill→take→write section is serialized by
2102
+ * [`calib_guard`] (a process-wide `try_lock`); a second concurrent calibration
2103
+ * fails fast with "another calibration is in progress". The arm flag is
2104
+ * thread-local (so a concurrent inference model can't contaminate the run), but
2105
+ * the running-max MAP is process-global, so serializing RUNS keeps two
2106
+ * calibrations from interleaving `record`/`take` on it. The map is CLEARED at
2107
+ * the very start so stale amax from a prior PANICKED run cannot leak into this
2108
+ * write.
2109
+ *
2110
+ * On ANY error before the final write, the partial amax is discarded and
2111
+ * `config.json` is left UNTOUCHED (a failed calibration must not mutate the
2112
+ * live model in place). A run that prefilled ZERO rows (empty dataset, or every
2113
+ * row tokenized to nothing) is likewise an ERROR that leaves `config.json`
2114
+ * untouched — a no-op calibration must not report a silent success. Returns the
2115
+ * number of projections calibrated (the count of collected amax entries); 0
2116
+ * means a real prefill ran but the model exercised no activation-fp8 sites (not
2117
+ * an nvidia-recipe checkpoint), and in that case `config.json` is left
2118
+ * UNCHANGED (no rewrite).
2119
+ */
2120
+ export declare function calibrateActivationAmaxRaw(
2121
+ modelPath: string,
2122
+ texts: Array<string>,
2123
+ calibSeq: number,
2124
+ ): Promise<number>;
2125
+
2395
2126
  /** Unified chat configuration shared by all model variants (Qwen3, Qwen3.5, Qwen3.5 MoE). */
2396
2127
  export interface ChatConfig {
2128
+ /**
2129
+ * Internal logical cache owner. The agent provider forwards Pi's stable
2130
+ * session id so model-global GDN sidecars can retain parent and child
2131
+ * branches independently. This does not namespace the physical paged KV
2132
+ * cache; exact token/extra-key hashes remain shareable across owners.
2133
+ */
2134
+ cacheOwnerId?: string | undefined;
2135
+ /**
2136
+ * Internal top-level owner for the bounded Qwen3.5 GDN sidecar store.
2137
+ * `cache_owner_id` may identify a child Pi session; this separately
2138
+ * identifies the current interactive root so /new and /resume can rotate
2139
+ * the protected branch without changing PagedAttention cache identity.
2140
+ */
2141
+ cacheRootOwnerId?: string | undefined;
2397
2142
  maxNewTokens?: number | undefined;
2398
2143
  temperature?: number | undefined;
2399
2144
  topK?: number | undefined;
@@ -2417,11 +2162,11 @@ export interface ChatConfig {
2417
2162
  frequencyPenalty?: number | undefined;
2418
2163
  /** Number of recent tokens to consider for frequency penalty (default: 20) */
2419
2164
  frequencyContextSize?: number | undefined;
2420
- /** Max consecutive identical tokens before stopping (default: 16, 0 = disabled) */
2165
+ /** Max consecutive identical tokens before stopping (default: 0 = disabled; opt in with a positive value) */
2421
2166
  maxConsecutiveTokens?: number | undefined;
2422
- /** Max n-gram repetitions before stopping (default: 3, 0 = disabled) */
2167
+ /** Max n-gram repetitions before stopping (default: 0 = disabled; opt in with a positive value) */
2423
2168
  maxNgramRepeats?: number | undefined;
2424
- /** Max pattern size for n-gram repetition detection (default: 64) */
2169
+ /** Max pattern size for n-gram repetition detection (default: 0 = disabled; opt in with a positive value) */
2425
2170
  ngramSize?: number | undefined;
2426
2171
  tools?: Array<ToolDefinition>;
2427
2172
  /**
@@ -2454,6 +2199,56 @@ export interface ChatConfig {
2454
2199
  * avoiding redundant computation for multi-turn conversations.
2455
2200
  */
2456
2201
  reuseCache?: boolean | undefined;
2202
+ /**
2203
+ * MTP: opt-in flag enabling the Multi-Token Prediction speculative decode
2204
+ * loop (pure-Rust eager; qwen3.5 dense and MoE). Requires the model
2205
+ * checkpoint to carry an MTP head (otherwise silently ignored). Default:
2206
+ * `false`.
2207
+ */
2208
+ enableMtp?: boolean | undefined;
2209
+ /**
2210
+ * MTP: number of draft tokens per speculative cycle.
2211
+ *
2212
+ * On Qwen3.5 native MTP heads it is clamped to `[1, 5]` by the verify
2213
+ * FFI contract, and when unset native code currently pins depth 1.
2214
+ * When `mtpAdaptiveDepth` is `true`, this value is used as the
2215
+ * throughput-policy seed and the expected-value policy's max depth.
2216
+ * Adaptive depth is opt-in; set `mtpAdaptiveDepth: true` explicitly to
2217
+ * enable it.
2218
+ *
2219
+ * Gemma4 external drafts (`draftModelPath`) resolve the field per draft
2220
+ * variant instead (`gemma4/model.rs` `resolve_params`, always from the
2221
+ * RAW config value — the engine's central `[1, 5]` clamp is an MTP-head
2222
+ * contract that does not apply to external drafts):
2223
+ * - DSpark: with both knobs unset, full draft blocks (the checkpoint's
2224
+ * block size — 7 tokens on `dspark_gemma4_12b_block7`) run behind a
2225
+ * short target-AR/DSpark break-even calibration. A short generation
2226
+ * budget that cannot finish calibration retains the fixed-block
2227
+ * schedule. An explicit
2228
+ * `mtpDepth` caps and pins the block unless `mtpAdaptiveDepth: true`
2229
+ * opts the guard back in; explicit `false` disables it.
2230
+ * - Assistant (Google `gemma-4-*-it-assistant`): an unset `mtpDepth`
2231
+ * drafts 3 tokens per cycle (`ASSISTANT_DEFAULT_DEPTH`), and an
2232
+ * explicit `mtpDepth` clamps to `[1, 8]` (`ASSISTANT_MAX_DEPTH`).
2233
+ *
2234
+ * `mtpAdaptiveDepth` is ignored for the Gemma4 assistant variant.
2235
+ */
2236
+ mtpDepth?: number | undefined;
2237
+ /**
2238
+ * MTP: when true, the decode loop runs the adaptive
2239
+ * depth policy. Default mode is a per-depth EMA hill-climb plus
2240
+ * DFlash-style 3-state machine `full | reduced | probe`.
2241
+ * `MLX_MTP_ADAPTIVE_DEPTH_MODE=expected-value` instead uses the
2242
+ * MTPLX-style intra-cycle expected-value gate, which deepens toward
2243
+ * `mtpDepth` by default (T=0 byte-parity verified); set
2244
+ * `MLX_MTP_EV_ALLOW_DEEPEN=0` to pin the base depth.
2245
+ * When false, the loop pins `mtpDepth` for every cycle.
2246
+ *
2247
+ * Default: false, except Gemma4 DSpark enables its measured break-even
2248
+ * guard when both this field and `mtpDepth` are unset. An explicit value
2249
+ * always wins over the family default.
2250
+ */
2251
+ mtpAdaptiveDepth?: boolean | undefined;
2457
2252
  }
2458
2253
 
2459
2254
  /** Chat message with tool calling support */
@@ -2466,10 +2261,34 @@ export interface ChatMessage {
2466
2261
  toolCalls?: Array<ToolCall>;
2467
2262
  /** Tool call ID this message is responding to (for tool messages) */
2468
2263
  toolCallId?: string;
2264
+ /**
2265
+ * Whether this tool-role message represents an errored tool result.
2266
+ *
2267
+ * Authoritative, structured signal of tool-call failure. Set to
2268
+ * `Some(true)` when the caller (e.g. the Anthropic
2269
+ * `tool_result.is_error === true` translator) wants the model to
2270
+ * treat the tool output as an error. It is exposed to the model's
2271
+ * chat template as `message.is_error`; Rust never rewrites `content`
2272
+ * or invents a model-facing error marker.
2273
+ */
2274
+ isError?: boolean;
2469
2275
  /** Reasoning content for thinking mode (used with <think> tags) */
2470
2276
  reasoningContent?: string;
2277
+ /**
2278
+ * Thinking mode used when this assistant message was generated.
2279
+ *
2280
+ * This is replay provenance, not a request override. Gemma4's disabled-
2281
+ * thinking generation prefix contains an explicit empty thought channel,
2282
+ * while an enabled-thinking turn that emitted no reasoning contains no
2283
+ * such channel. Keeping the historical mode on the message lets the chat
2284
+ * template reproduce either byte sequence even when a later request
2285
+ * changes its current thinking setting.
2286
+ */
2287
+ thinkingEnabled?: boolean;
2471
2288
  /** Image data for VLM models (encoded image bytes: PNG/JPEG, passed as Uint8Array/Buffer) */
2472
2289
  images?: Array<Uint8Array> | undefined;
2290
+ /** Audio data for unified Gemma 4 (encoded audio bytes: WAV, passed as Uint8Array/Buffer) */
2291
+ audio?: Array<Uint8Array> | undefined;
2473
2292
  }
2474
2293
 
2475
2294
  /** Unified chat result shared by all model variants (Qwen3, Qwen3.5, Qwen3.5 MoE). */
@@ -2477,24 +2296,45 @@ export interface ChatResult {
2477
2296
  text: string;
2478
2297
  toolCalls: Array<ToolCallResult>;
2479
2298
  thinking?: string;
2299
+ /**
2300
+ * Effective `enable_thinking` boolean passed to the model-provided
2301
+ * chat template for this turn. This is replay provenance and is
2302
+ * intentionally distinct from the family's decode-time
2303
+ * [`ThinkingSetup`](crate::engine::ThinkingSetup).
2304
+ */
2305
+ thinkingEnabled: boolean;
2480
2306
  numTokens: number;
2481
2307
  promptTokens: number;
2482
2308
  reasoningTokens: number;
2483
2309
  finishReason: string;
2484
2310
  rawText: string;
2311
+ /**
2312
+ * Reasoning-redacted raw output computed from the same token-aware
2313
+ * boundary as `raw_text`. Session wrappers may request full reasoning
2314
+ * internally for deterministic replay, then expose this safe view to a
2315
+ * caller that set `includeReasoning: false`.
2316
+ */
2317
+ publicRawText?: string | undefined;
2318
+ /**
2319
+ * Number of prompt tokens served from the reused KV-cache prefix.
2320
+ *
2321
+ * When the native prefix-cache machinery successfully matches the new
2322
+ * prompt against the cached conversation history (via
2323
+ * `verify_cache_prefix_direct`), only the trailing delta is re-prefilled
2324
+ * and this field reports the length of the reused prefix. `0` when
2325
+ * the cache was missed or disabled and the full prompt had to be
2326
+ * re-prefilled.
2327
+ */
2328
+ cachedTokens: number;
2485
2329
  /** Performance metrics (present when `reportPerformance: true` in config) */
2486
2330
  performance?: PerformanceMetrics;
2487
2331
  }
2488
2332
 
2489
- /** Chat message role (lowercase values matching standard convention) */
2333
+ /** Chat message role (lowercase values matching standard convention). */
2490
2334
  export declare const enum ChatRole {
2491
- /** User message */
2492
2335
  User = 'user',
2493
- /** Assistant response */
2494
2336
  Assistant = 'assistant',
2495
- /** System prompt */
2496
2337
  System = 'system',
2497
- /** Tool response */
2498
2338
  Tool = 'tool',
2499
2339
  }
2500
2340
 
@@ -2505,10 +2345,36 @@ export interface ChatStreamChunk {
2505
2345
  finishReason?: string;
2506
2346
  toolCalls?: Array<ToolCallResult>;
2507
2347
  thinking?: string;
2348
+ /**
2349
+ * Effective template `enable_thinking` value. Present on the terminal
2350
+ * chunk only; incremental text/reasoning chunks leave it unset.
2351
+ */
2352
+ thinkingEnabled?: boolean | undefined;
2508
2353
  numTokens?: number;
2509
2354
  promptTokens?: number;
2510
2355
  reasoningTokens?: number;
2511
2356
  rawText?: string;
2357
+ /** Reasoning-redacted counterpart to `raw_text` on terminal chunks. */
2358
+ publicRawText?: string | undefined;
2359
+ /**
2360
+ * Whether terminal `text` is the authoritative parsed assistant content.
2361
+ * Generic emitters set true; Gemma streams visible content exclusively as
2362
+ * deltas and set false so session history commits the accumulated text.
2363
+ */
2364
+ textAuthoritative?: boolean | undefined;
2365
+ /**
2366
+ * Number of prompt tokens served from the reused KV-cache prefix on
2367
+ * this turn. Populated on the terminal chunk (`done == true`) only;
2368
+ * `None` on mid-stream delta chunks.
2369
+ *
2370
+ * Zero on a cache miss or disabled reuse; equal to the matched
2371
+ * prefix length on a hit. Mirrors `ChatResult.cached_tokens`
2372
+ * verbatim so session-aware streaming consumers can observe
2373
+ * prefix-cache reuse without round-tripping to the non-streaming
2374
+ * path. Non-terminal chunks always carry `None` — only the
2375
+ * terminal chunk is authoritative.
2376
+ */
2377
+ cachedTokens?: number | undefined;
2512
2378
  /** Performance metrics (only present in the final chunk when `reportPerformance: true`) */
2513
2379
  performance?: PerformanceMetrics;
2514
2380
  /**
@@ -2531,16 +2397,181 @@ export interface ClassifyRotateResult {
2531
2397
  image: Uint8Array;
2532
2398
  }
2533
2399
 
2534
- /** Statistics about cleanup operations (NAPI wrapper) */
2535
- export interface CleanupStats {
2536
- /** Number of training steps deleted */
2537
- stepsDeleted: number;
2538
- /** Number of generations deleted */
2539
- generationsDeleted: number;
2540
- /** Number of tool calls deleted */
2541
- toolCallsDeleted: number;
2542
- /** Number of logs deleted */
2543
- logsDeleted: number;
2400
+ /** Statistics about cleanup operations (NAPI wrapper) */
2401
+ export interface CleanupStats {
2402
+ /** Number of training steps deleted */
2403
+ stepsDeleted: number;
2404
+ /** Number of generations deleted */
2405
+ generationsDeleted: number;
2406
+ /** Number of tool calls deleted */
2407
+ toolCallsDeleted: number;
2408
+ /** Number of logs deleted */
2409
+ logsDeleted: number;
2410
+ }
2411
+
2412
+ /**
2413
+ * Flush every accepted cold-tier block to disk, blocking until the
2414
+ * background writer has fsync+renamed each write enqueued before this call,
2415
+ * or `timeout_ms` elapses. Returns `true` when the drain completed — or when
2416
+ * the tier was never opened (nothing to flush) — and `false` on timeout.
2417
+ *
2418
+ * Called from the agent's one-shot (`mlx agent -p`) shutdown so a prompt's
2419
+ * just-persisted prefix blocks reach the drive before the process exits,
2420
+ * rather than being abandoned in the write queue.
2421
+ *
2422
+ * The payload flush is `fsync(2)`, not `F_FULLFSYNC`: a drained block
2423
+ * survives process death and kernel panic, but a sudden power loss can leave
2424
+ * it torn. Torn objects fail the payload checksum on the next read and are
2425
+ * pruned as a miss, so the cost is a recomputed prefix, never wrong state.
2426
+ */
2427
+ export declare function coldCacheDrain(timeoutMs: number): boolean;
2428
+
2429
+ /**
2430
+ * Return a snapshot of the process-wide cold tier. Read-only: never opens
2431
+ * the tier itself, so it reports `enabled: false` until inference first
2432
+ * initializes the tier.
2433
+ *
2434
+ * The source snapshot is DESTRUCTURED rather than field-accessed, so a
2435
+ * counter added to `ColdCacheStats` and forgotten here fails to compile.
2436
+ * The failure this guards is silent and has already happened twice: a native
2437
+ * counter that never reaches this struct reaches no JS consumer either, and
2438
+ * nothing downstream can tell "the counter is zero" from "the counter was
2439
+ * never carried across".
2440
+ */
2441
+ export declare function coldCacheStats(): ColdCacheStats;
2442
+
2443
+ /**
2444
+ * Snapshot of the process-wide SSD cold tier for paged prefix blocks.
2445
+ * Counters are cumulative since the tier was opened; all numeric values
2446
+ * are returned as `f64` to avoid BigInt round-trips in JS.
2447
+ */
2448
+ export interface ColdCacheStats {
2449
+ /**
2450
+ * `false` until the tier is first opened by inference, or when opening
2451
+ * failed (fail-open: inference then runs without persistence).
2452
+ */
2453
+ enabled: boolean;
2454
+ /** Cache root directory (empty while disabled). */
2455
+ root: string;
2456
+ /** Disk quota in bytes. */
2457
+ quotaBytes: number;
2458
+ /** Blocks restored from disk after validation. */
2459
+ hits: number;
2460
+ /** Lookups that found no usable block (includes corrupt entries). */
2461
+ misses: number;
2462
+ /**
2463
+ * Objects accepted onto the background write queue — K/V blocks and
2464
+ * family state sidecars alike, since both take a slot in the same queue.
2465
+ * `coldSidecarEnqueued` counts a subset of this; never sum them.
2466
+ */
2467
+ enqueued: number;
2468
+ /**
2469
+ * Writes REFUSED at admission because the bounded queue was full. Same
2470
+ * object scope as `enqueued`. Disjoint from `writeErrors`, which counts
2471
+ * accepted writes that then failed to land — never sum the two into one
2472
+ * "lost writes" number.
2473
+ */
2474
+ queueDrops: number;
2475
+ /**
2476
+ * Bytes that LANDED, credited after the payload sync, the commit rename
2477
+ * and the directory fsync all succeeded. Not an enqueue-time estimate:
2478
+ * a failed write credits nothing here and one `writeErrors` instead.
2479
+ */
2480
+ bytesWritten: number;
2481
+ /** Total bytes read back on validated hits. */
2482
+ bytesRestored: number;
2483
+ /** Entries evicted to respect the quota / free-space reserve. */
2484
+ evictions: number;
2485
+ /** Entries that failed checksum/identity validation and were removed. */
2486
+ corruptions: number;
2487
+ /**
2488
+ * Writes the queue accepted that never reached disk — a read-only, full
2489
+ * or unmounted cache root, a failed rename, a failed fsync. The writer is
2490
+ * fail-open and reports the error to nobody, so without this a cache that
2491
+ * stores nothing at all still looks perfectly healthy.
2492
+ */
2493
+ writeErrors: number;
2494
+ /**
2495
+ * Restores refused before any block was looked up. Neither a hit nor a
2496
+ * miss — so a refused restore reads as `0/0`, exactly like a turn that
2497
+ * never consulted the tier.
2498
+ */
2499
+ restoreDeclines: number;
2500
+ }
2501
+
2502
+ /**
2503
+ * The native cold-restore allowlist, exposed so a test can assert it agrees
2504
+ * exactly with the TypeScript `COLD_TIER_RESTORE_FAMILIES` set.
2505
+ */
2506
+ export declare function coldRestoreFamilies(): Array<string>;
2507
+
2508
+ /**
2509
+ * Return the process-wide sidecar counters. Unlike [`cold_cache_stats`] this
2510
+ * never consults the tier at all, so it is valid before any inference has run
2511
+ * and reports honestly even when the tier failed to open.
2512
+ *
2513
+ * The plain-Rust [`cold_sidecar_telemetry`] stays as it is: the parity harness
2514
+ * and the in-crate tests destructure `ColdSidecarTelemetry`, and a napi
2515
+ * `#[napi(object)]` return type cannot serve both.
2516
+ */
2517
+ export declare function coldSidecarStats(): ColdSidecarStats;
2518
+
2519
+ /**
2520
+ * Snapshot of [`ColdSidecarTelemetry`] for JS. Counters are cumulative since
2521
+ * process start; all values are `f64` to avoid BigInt round-trips.
2522
+ *
2523
+ * Separate from [`ColdCacheStatsJs`] because this one isolates SIDECARS — the
2524
+ * recurrent and sliding-window state that lives outside the pool — while that
2525
+ * one is scoped to the write queue as a whole. Both structs carry an
2526
+ * `enqueued` and a `queue_drops`, and they are NOT disjoint: a sidecar
2527
+ * admission bumps both, so these are a subset of those and summing them
2528
+ * double-counts. The JS side keeps them apart by prefix (`coldEnqueued` vs
2529
+ * `coldSidecarEnqueued`); read the sidecar pair when you need "did the family
2530
+ * state persist?", the block pair when you need "is the writer keeping up?".
2531
+ */
2532
+ export interface ColdSidecarStats {
2533
+ /**
2534
+ * Turns that reached a family's sidecar capture at all. Every other
2535
+ * counter here is a sub-count of this one, so `captureReached == 0`
2536
+ * separates "the finalize path never calls the capture" from "the capture
2537
+ * ran and declined".
2538
+ */
2539
+ captureReached: number;
2540
+ /**
2541
+ * Turns whose persisted K/V chain covered no whole block, so there was no
2542
+ * prefix to anchor recurrent state under.
2543
+ */
2544
+ chainEmpty: number;
2545
+ /**
2546
+ * Turns whose chain covered blocks but where no retained checkpoint sat at
2547
+ * or below its reach.
2548
+ */
2549
+ boundarySkips: number;
2550
+ /**
2551
+ * Turns that selected a boundary already on disk — nothing written, and
2552
+ * nothing needed to be. The steady state of a repeated prompt, and the
2553
+ * only thing that tells a healthy run from a collapsed ladder.
2554
+ */
2555
+ alreadyPersisted: number;
2556
+ /** Sidecars handed to the bounded writer queue. */
2557
+ enqueued: number;
2558
+ /** Sidecars the bounded writer queue refused because it was full. */
2559
+ queueDrops: number;
2560
+ /**
2561
+ * Restored sidecars a family actually INSTALLED as its live per-turn
2562
+ * state. The one read-side counter, and the only signal that separates
2563
+ * "restored and used" from "restored and silently re-derived by a full
2564
+ * O(prefix) replay" — every other counter, and text parity itself, is
2565
+ * satisfied by the replay.
2566
+ */
2567
+ installed: number;
2568
+ /**
2569
+ * Restores a family THREW AWAY after the walk served them, restarting the
2570
+ * turn cold. Unlike `ColdCacheStats.restoreDeclines` this one comes AFTER
2571
+ * real `coldHits` and `coldBytesRestored`, so the turn looks like it
2572
+ * reused a prefix right up to the point where it recomputed all of it.
2573
+ */
2574
+ restoreSuppressed: number;
2544
2575
  }
2545
2576
 
2546
2577
  /**
@@ -2579,7 +2610,11 @@ export interface ConversionOptions {
2579
2610
  quantBits?: number;
2580
2611
  /** Quantization group size (default: 64 for affine, 32 for mxfp8) */
2581
2612
  quantGroupSize?: number;
2582
- /** Quantization mode: "affine" (default) or "mxfp8" */
2613
+ /**
2614
+ * Quantization mode: "affine" (default), "mxfp4", "mxfp8", "nvfp4", or
2615
+ * "sym8" (per-output-channel symmetric int8; qwen3_5 + qwen3_5_moe + lfm2/lfm2_moe + gemma4,
2616
+ * implies bits=8, no group_size — consciously NOT mlx-lm-loadable)
2617
+ */
2583
2618
  quantMode?: string;
2584
2619
  /**
2585
2620
  * Quantization recipe for per-layer mixed-bit quantization.
@@ -2591,6 +2626,35 @@ export interface ConversionOptions {
2591
2626
  * Improves quantization quality by amplifying important weight channels.
2592
2627
  */
2593
2628
  imatrixPath?: string;
2629
+ /**
2630
+ * Upgrade quantization to micro-scaling FP (mxfp4 / mxfp8).
2631
+ * When true, applies after the recipe predicate: eligible 8-bit affine
2632
+ * decisions become mxfp8 and 4-bit become mxfp4. Kept affine (not upgraded):
2633
+ * affine-only loaders (lm_head, embed_tokens, router.proj,
2634
+ * embedding_projection) at their recipe bits, MoE router gates (8-bit affine),
2635
+ * and the recipe-pinned attention/GDN projections (o_proj / out_proj /
2636
+ * in_proj_a / in_proj_b, 8-bit affine). Requires `quant_mode = "affine"`.
2637
+ * Forces `group_size = 32` for upgraded layers.
2638
+ */
2639
+ quantMxfp?: boolean;
2640
+ /**
2641
+ * Optional Qwen MTP quantization policy: "off" (default), "cyankiwi", "all",
2642
+ * or "split" (alias "drafter").
2643
+ * "cyankiwi" keeps mtp.fc dense and quantizes only the MTP layer linears as
2644
+ * 4-bit affine group_size=32. For dense `qwen3_5` the quantized linears are
2645
+ * emitted into an MTPLX-compatible mtp.safetensors sidecar; for MoE
2646
+ * (`qwen3_5_moe`) there is no sidecar — they are quantized in place and stored
2647
+ * inline in the main safetensors shards.
2648
+ * "all" additionally quantizes mtp.fc. For dense `qwen3_5` the quantized MTP
2649
+ * linears land in the mtp.safetensors sidecar; for MoE (`qwen3_5_moe`) there
2650
+ * is no sidecar — they are quantized in place and stored inline in the main
2651
+ * safetensors shards.
2652
+ * "split"/"drafter" emits a body checkpoint with NO mtp.* tensors plus a
2653
+ * separate `mtp-drafter/` directory in mlx-vlm's `qwen3_5_mtp` format
2654
+ * (bare-keyed MTP head, format:mlx). It does NOT require --quantize/--q-recipe;
2655
+ * the body may be bf16 or already-quantized and the MTP head stays bf16.
2656
+ */
2657
+ quantMtp?: string;
2594
2658
  }
2595
2659
 
2596
2660
  export interface ConversionResult {
@@ -2662,11 +2726,11 @@ export declare function createRandomQwen35Checkpoint(config: Qwen35Config, saveP
2662
2726
  /**
2663
2727
  * Create a random-init Qwen3.5 MoE model and save it to disk.
2664
2728
  *
2665
- * Spawns a dedicated `ModelThread<Qwen35MoeCmd>` whose init builds a fresh
2666
- * random-weight `Qwen35MoeInner` directly, then dispatches
2667
- * `Qwen35MoeCmd::SaveModel` on that thread. The thread is dropped at the end
2668
- * of the promise, so the in-memory model is released once the checkpoint has
2669
- * been written. Used by TypeScript test fixtures that need an on-disk
2729
+ * Spawns a dedicated model thread whose init runs
2730
+ * [`create_random_qwen35_moe_checkpoint_sync`] (random-init inner + save);
2731
+ * the thread holds no state and is dropped once the promise resolves, so
2732
+ * the in-memory model is released as soon as the checkpoint has been
2733
+ * written. Used by TypeScript test fixtures that need an on-disk
2670
2734
  * checkpoint without keeping a NAPI model instance alive.
2671
2735
  */
2672
2736
  export declare function createRandomQwen35MoeCheckpoint(config: Qwen35MoeConfig, savePath: string): Promise<undefined>;
@@ -2717,6 +2781,8 @@ export declare const enum DType {
2717
2781
  BFloat16 = 3,
2718
2782
  Uint32 = 4,
2719
2783
  Uint8 = 5,
2784
+ /** Signed int8 — sym8 per-output-channel symmetric quantized weights. */
2785
+ Int8 = 6,
2720
2786
  }
2721
2787
 
2722
2788
  /** Document element type */
@@ -2809,6 +2875,26 @@ export interface FunctionParameters {
2809
2875
  required?: Array<string>;
2810
2876
  }
2811
2877
 
2878
+ /**
2879
+ * How many GDN prefix checkpoints the native store holds across all owners
2880
+ * ([`GDN_PREFIX_CHECKPOINT_LIMIT`]), exposed for the same reason
2881
+ * [`cold_restore_families`] is: it is one half of a cross-language invariant
2882
+ * and nothing else carries it over the boundary.
2883
+ *
2884
+ * The other half is `MAX_CONCURRENCY` in
2885
+ * `packages/agent/src/extensions/subagent.ts`. The store's demand is
2886
+ * `MAX_CONCURRENCY + 1` — one owner per concurrent child loop plus the root
2887
+ * session — and the cliff sits exactly one owner past the cap, where every
2888
+ * owner holds a single entry and the store is still over it, so each publish
2889
+ * takes somebody's last checkpoint. `retention_sim` measures that as 0 blind
2890
+ * turns at five owners and 28 of 40 at six.
2891
+ *
2892
+ * No Rust gate can see a TypeScript-only edit, so raising the fleet alone
2893
+ * would land in that regime with every Rust gate still green.
2894
+ * `packages/agent/__test__/gdn-checkpoint-capacity.test.ts` is what stops it.
2895
+ */
2896
+ export declare function gdnPrefixCheckpointLimit(): number;
2897
+
2812
2898
  /**
2813
2899
  * Gemma 4 model configuration (dense variant).
2814
2900
  *
@@ -2843,6 +2929,20 @@ export interface Gemma4Config {
2843
2929
  /** Head dimension for global layers. If None, uses head_dim. */
2844
2930
  globalHeadDim?: number;
2845
2931
  attentionKEqV: boolean;
2932
+ /**
2933
+ * True for the unified multimodal Gemma 4 checkpoint
2934
+ * (`model_type == "gemma4_unified"` or
2935
+ * `architectures[0] == "Gemma4UnifiedForConditionalGeneration"`).
2936
+ * The text decoder is shared, but the unified checkpoint carries
2937
+ * vision/audio embedder weights that must be dropped in a text-only load.
2938
+ */
2939
+ isUnified: boolean;
2940
+ /**
2941
+ * `text_config.use_bidirectional_attention` from the unified checkpoint
2942
+ * (e.g. `"vision"`). Parsed for a stable struct surface; the text-only
2943
+ * decode path does not consume it.
2944
+ */
2945
+ useBidirectionalAttention?: string;
2846
2946
  finalLogitSoftcapping?: number;
2847
2947
  perLayerInputEmbeds: boolean;
2848
2948
  hiddenSizePerLayerInput?: number;
@@ -2861,10 +2961,97 @@ export interface Gemma4Config {
2861
2961
  topKExperts?: number;
2862
2962
  moeIntermediateSize?: number;
2863
2963
  visionConfig?: Gemma4VisionConfig;
2964
+ /**
2965
+ * Encoder-free vision config for the unified multimodal checkpoint.
2966
+ * `Some` only when `is_unified` and the checkpoint carries a
2967
+ * `vision_config` sub-dict. Disjoint from `vision_config` (the SigLIP
2968
+ * path) — the unified vision embedder is built from this instead.
2969
+ */
2970
+ unifiedVisionConfig?: UnifiedVisionConfig;
2864
2971
  imageTokenId?: number;
2865
2972
  boiTokenId?: number;
2866
2973
  eoiTokenId?: number;
2867
2974
  visionSoftTokensPerImage?: number;
2975
+ /**
2976
+ * True when the checkpoint declares an `audio_config` sub-dict. Parallels
2977
+ * `unified_vision_config.is_some()`; gates the un-drop + load of
2978
+ * `embed_audio` weights and the audio merge path.
2979
+ */
2980
+ hasAudio: boolean;
2981
+ /**
2982
+ * Audio placeholder token id (258881). Each `<audio>` placeholder expands to
2983
+ * `boa + audio_token × n_frames + eoa`.
2984
+ */
2985
+ audioTokenId?: number;
2986
+ /** Begin-of-audio token id (256000), emitted before the audio token run. */
2987
+ boaTokenId?: number;
2988
+ /**
2989
+ * End-of-audio token id, parsed from the config's `eoa_token_index` (258883).
2990
+ * A real appended token (like `eoi`), despite the "index" name.
2991
+ */
2992
+ eoaTokenId?: number;
2993
+ /**
2994
+ * Raw audio samples per audio token (640 = 40 ms @ 16 kHz), from
2995
+ * `audio_config.audio_samples_per_token`. Frame size for the encoder-free
2996
+ * pad+reshape feature extractor.
2997
+ */
2998
+ audioSamplesPerToken?: number;
2999
+ /**
3000
+ * GPU memory budget for paged KV cache in megabytes.
3001
+ * Only used when `use_block_paged_cache` is true.
3002
+ * Default: auto-sized to cover `max_position_embeddings` for the
3003
+ * physical full-attention layers.
3004
+ */
3005
+ pagedCacheMemoryMb?: number | undefined;
3006
+ /**
3007
+ * Block size for paged attention (tokens per block).
3008
+ * Only used when `use_block_paged_cache` is true.
3009
+ * Default: 16.
3010
+ */
3011
+ pagedBlockSize?: number | undefined;
3012
+ /**
3013
+ * Use the new block-paged KV cache adapter (`PagedKVCacheAdapter`).
3014
+ *
3015
+ * When `Some(true)` or unset (the default), `Gemma4Inner` builds
3016
+ * model-independent KV-cache specs, groups them, and allocates a
3017
+ * `BlockAllocator` + `LayerKVPool` pair for physical full-attention
3018
+ * layers. Sliding-window layers still use `RotatingKVCache` until
3019
+ * true paged sliding-window groups are wired. KV-shared layers are
3020
+ * aliases: they reuse their anchor's cache slot and do not allocate
3021
+ * separate physical storage.
3022
+ *
3023
+ * Default: `true` (paged adapter on; opt-out via
3024
+ * `use_block_paged_cache: false` in `config.json` to use the flat
3025
+ * (non-paged) all-`Gemma4LayerCache` path instead). Parity between
3026
+ * the two paths is verified by
3027
+ * `crates/mlx-core/tests/gemma4_paged_vs_flat_parity.rs` against
3028
+ * real Gemma-4-E2B weights.
3029
+ */
3030
+ useBlockPagedCache?: boolean | undefined;
3031
+ /**
3032
+ * Persist full paged KV blocks — and gemma4's out-of-pool sliding-window
3033
+ * state, as a cold-tier sidecar — to the SSD cold tier so warm prefixes
3034
+ * survive process restarts. Off unless explicitly enabled.
3035
+ *
3036
+ * An EXPLICIT value here is authoritative and beats the ambient
3037
+ * `MLX_PERSIST_PAGED_CACHE` default (`cold_tier::resolve_persist_cold`).
3038
+ */
3039
+ persistPagedCache?: boolean | undefined;
3040
+ }
3041
+
3042
+ /** Optional load-time settings for [`Gemma4Model::load`]. */
3043
+ export interface Gemma4LoadOptions {
3044
+ /**
3045
+ * Directory of a draft checkpoint (config.json + safetensors) to load
3046
+ * alongside the target model for speculative decoding — either a
3047
+ * DSpark draft or a Google assistant draft; the kind is probed from
3048
+ * the draft config.json. When omitted, `<model_path>/draft/` is loaded
3049
+ * automatically when present. Draft decoding runs only on the flat
3050
+ * KV-cache path: setting this while the model config explicitly enables
3051
+ * `use_block_paged_cache` is a hard load error, and an unset
3052
+ * `use_block_paged_cache` is forced to `false`.
3053
+ */
3054
+ draftModelPath?: string;
2868
3055
  }
2869
3056
 
2870
3057
  /**
@@ -2937,19 +3124,19 @@ export interface GenerationConfig {
2937
3124
  /** Number of recent tokens to consider for frequency penalty (default: 20) */
2938
3125
  frequencyContextSize?: number;
2939
3126
  /**
2940
- * Stop if same token repeats this many times consecutively (default: 16)
2941
- * Set to 0 to disable. Prevents OOM from degenerate repetitive generation.
3127
+ * Stop if same token repeats this many times consecutively (default: 0 = disabled).
3128
+ * Opt in by setting a positive value to guard against degenerate repetitive generation.
2942
3129
  */
2943
3130
  maxConsecutiveTokens?: number;
2944
3131
  /**
2945
- * Stop if a pattern repeats this many times consecutively (default: 3)
2946
- * Set to 0 to disable. Detects patterns like "A B A B A B".
3132
+ * Stop if a pattern repeats this many times consecutively (default: 0 = disabled).
3133
+ * Opt in with a positive value to detect patterns like "A B A B A B".
2947
3134
  * Uses range-based detection: checks all pattern sizes from 2 to ngram_size.
2948
3135
  */
2949
3136
  maxNgramRepeats?: number;
2950
3137
  /**
2951
- * Maximum pattern size for repetition detection (default: 64)
2952
- * All pattern sizes from 2 up to this value are checked each decode step.
3138
+ * Maximum pattern size for repetition detection (default: 0 = disabled).
3139
+ * When enabled, all pattern sizes from 2 up to this value are checked each decode step.
2953
3140
  * Larger values catch long phrase-level repetition common in small models.
2954
3141
  */
2955
3142
  ngramSize?: number;
@@ -2964,29 +3151,6 @@ export interface GenerationConfig {
2964
3151
  * Set to 0 to disable chunking and process the entire prompt at once.
2965
3152
  */
2966
3153
  prefillStepSize?: number;
2967
- /**
2968
- * KV cache quantization bits (default: 16 = no quantization)
2969
- * - 16: Full precision (bfloat16/float16), no quantization
2970
- * - 8: 8-bit quantization, ~2x memory savings, minimal quality loss
2971
- * - 4: 4-bit quantization, ~4x memory savings, some quality degradation
2972
- *
2973
- * Quantized KV cache is useful for long sequences where memory becomes a bottleneck.
2974
- * Note: Adds dequantization overhead per forward pass.
2975
- */
2976
- kvCacheBits?: number;
2977
- /**
2978
- * KV cache quantization group size (default: 64)
2979
- * Number of elements per quantization group. Smaller groups = better accuracy
2980
- * but more overhead from storing scales/biases.
2981
- * Only used when kv_cache_bits is 4 or 8.
2982
- */
2983
- kvCacheGroupSize?: number;
2984
- /**
2985
- * Number of draft tokens to generate speculatively (default: 5)
2986
- * Only used when a draft model is provided for speculative decoding.
2987
- * Higher values can increase throughput but may reduce acceptance rate.
2988
- */
2989
- numDraftTokens?: number;
2990
3154
  }
2991
3155
 
2992
3156
  export interface GenerationProfile {
@@ -3010,6 +3174,24 @@ export interface GenerationProfile {
3010
3174
  timeToFirstTokenMs: number;
3011
3175
  /** Per-phase breakdown. */
3012
3176
  phases: Array<PhaseProfile>;
3177
+ /**
3178
+ * MTP speculative decode: mean accepted *draft* tokens per cycle
3179
+ * (excludes the always-verified token). Historical drafts-only metric.
3180
+ */
3181
+ mtpMeanAcceptedTokens?: number;
3182
+ /**
3183
+ * MTP speculative decode: mean *committed* tokens per cycle, INCLUDING
3184
+ * the always-verified token (`mtp_accepted_drafts_total / mtp_cycles
3185
+ * + 1.0`). mlx-vlm-comparable headline; equals mlx-vlm's
3186
+ * `(accepted_drafts + rounds) / rounds` (`common.py:247`).
3187
+ */
3188
+ mtpMeanAcceptedTokensTotal?: number;
3189
+ /** MTP speculative decode: per-draft-position acceptance rate. */
3190
+ mtpAcceptanceByPosition?: Array<number>;
3191
+ /** MTP speculative decode: number of draft+verify cycles executed. */
3192
+ mtpCycles?: number;
3193
+ /** MTP speculative decode: mean attempted draft depth per cycle. */
3194
+ mtpMeanDepth?: number;
3013
3195
  /** Memory snapshot before generation. */
3014
3196
  memoryBefore?: MemorySnapshot;
3015
3197
  /** Memory snapshot after generation. */
@@ -3036,8 +3218,8 @@ export interface GenerationWithToolCalls {
3036
3218
  toolCalls: Array<ToolCallRecord>;
3037
3219
  }
3038
3220
 
3039
- /** Get expected weight keys for PaddleOCR-VL model */
3040
- export declare function getExpectedWeightKeys(): Array<string>;
3221
+ /** Sample MLX's GPU memory counters. See [`GpuMemorySnapshot`]. */
3222
+ export declare function getMemorySnapshot(): GpuMemorySnapshot;
3041
3223
 
3042
3224
  /** Retrieve all collected profiling data as a `ProfilingSession`. */
3043
3225
  export declare function getProfilingData(): ProfilingSession;
@@ -3047,6 +3229,13 @@ export interface GgufConversionOptions {
3047
3229
  inputPath: string;
3048
3230
  /** Output directory for converted SafeTensors model */
3049
3231
  outputDir: string;
3232
+ /**
3233
+ * Optional directory containing the authoritative HuggingFace config and
3234
+ * tokenizer/processor assets. GGUF metadata is not rich enough to recreate
3235
+ * unified Gemma4 config fields such as head_dim, layer_types, vision, and
3236
+ * audio configuration exactly.
3237
+ */
3238
+ configSourceDir?: string;
3050
3239
  /** Target dtype: "float32", "float16", "bfloat16" (default: keep original) */
3051
3240
  dtype?: string;
3052
3241
  /** Enable verbose logging */
@@ -3080,6 +3269,25 @@ export interface GgufConversionOptions {
3080
3269
  * This makes the safetensors compatible with mlx-vlm.
3081
3270
  */
3082
3271
  vlmKeyPrefix?: boolean;
3272
+ /**
3273
+ * Upgrade quantization to micro-scaling FP (mxfp4 / mxfp8).
3274
+ * When true, applies after the recipe predicate: eligible 8-bit affine
3275
+ * decisions become mxfp8 and 4-bit become mxfp4. Kept affine (not upgraded):
3276
+ * affine-only loaders (lm_head, embed_tokens, router.proj,
3277
+ * embedding_projection) at their recipe bits, MoE router gates (8-bit affine),
3278
+ * and the recipe-pinned attention/GDN projections (o_proj / out_proj /
3279
+ * in_proj_a / in_proj_b, 8-bit affine). Requires `quant_mode = "affine"`.
3280
+ * Forces `group_size = 32` for upgraded layers.
3281
+ */
3282
+ quantMxfp?: boolean;
3283
+ /**
3284
+ * Import ggml Q4_K / Q5_K / Q6_K tensors as MLX K-quant arrays instead of
3285
+ * rejecting them (default: false). The blocks are repacked, never
3286
+ * dequantized, so the output keeps the source file's weights and byte size.
3287
+ * With this off, Q6_K remains the Gemma4 token-embedding BF16 fallback and
3288
+ * Q4_K / Q5_K are an error.
3289
+ */
3290
+ importKQuants?: boolean;
3083
3291
  }
3084
3292
 
3085
3293
  export interface GgufConversionResult {
@@ -3095,6 +3303,33 @@ export interface GpuInfo {
3095
3303
  architectureGen: number;
3096
3304
  }
3097
3305
 
3306
+ /**
3307
+ * Snapshot of MLX's GPU memory counters at this instant. All values
3308
+ * are in bytes. On Apple Silicon, GPU and CPU share unified memory,
3309
+ * so these are NOT a separate "VRAM" pool — they reflect MLX's own
3310
+ * tracking of `StorageModePrivate` Metal buffers (model weights,
3311
+ * `LayerKVPool`, transient intermediate tensors) attributed to the
3312
+ * MLX runtime in this process.
3313
+ *
3314
+ * Useful for live observability during long-running sessions:
3315
+ *
3316
+ * ```js
3317
+ * const { getMemorySnapshot } = require('@mlx-node/core');
3318
+ * setInterval(() => {
3319
+ * const m = getMemorySnapshot();
3320
+ * console.log(`active=${(m.activeBytes/1e9).toFixed(2)}GB peak=${(m.peakBytes/1e9).toFixed(2)}GB cache=${(m.cacheBytes/1e9).toFixed(2)}GB`);
3321
+ * }, 1000);
3322
+ * ```
3323
+ */
3324
+ export interface GpuMemorySnapshot {
3325
+ /** Current actively-used GPU buffer bytes (excludes cache pool). */
3326
+ activeBytes: number;
3327
+ /** Peak GPU buffer bytes since the last `resetPeakMemory()` call. */
3328
+ peakBytes: number;
3329
+ /** Bytes held in MLX's caching allocator (released by `clearCache`). */
3330
+ cacheBytes: number;
3331
+ }
3332
+
3098
3333
  /** Configuration for the GRPO training engine */
3099
3334
  export interface GrpoEngineConfig {
3100
3335
  /** Learning rate (default: 1e-6) */
@@ -3356,6 +3591,69 @@ export interface Lfm2Config {
3356
3591
  eosTokenId: number;
3357
3592
  bosTokenId: number;
3358
3593
  padTokenId: number;
3594
+ /**
3595
+ * GPU memory budget for paged KV cache in megabytes.
3596
+ * Only used when `use_block_paged_cache` is true.
3597
+ * Default: 2048 (2GB).
3598
+ */
3599
+ pagedCacheMemoryMb?: number | undefined;
3600
+ /**
3601
+ * Block size for paged attention (tokens per block).
3602
+ * Only used when `use_block_paged_cache` is true.
3603
+ * Default: 16.
3604
+ */
3605
+ pagedBlockSize?: number | undefined;
3606
+ /**
3607
+ * Use the new block-paged KV cache adapter (`PagedKVCacheAdapter`).
3608
+ *
3609
+ * Default: `true` since 2026-04-28 (parity-verified via
3610
+ * `crates/mlx-core/tests/lfm2_paged_vs_flat_parity.rs` against real
3611
+ * LFM2.5-1.2B weights: byte-equal greedy decode + prefix-reuse
3612
+ * byte-equal at BF16). Wired through
3613
+ * `Lfm2DecoderLayer::forward_paged_or_flat`.
3614
+ *
3615
+ * Per-layer routing: LFM2's hybrid architecture means only
3616
+ * `full_attention` layers go through the paged adapter; conv layers
3617
+ * stay on the existing flat `Lfm2LayerCache::Conv(ArraysCache)`
3618
+ * storage regardless of this flag. The `LayerKVPool` is sized to
3619
+ * the count of `full_attention` layers and indexed by
3620
+ * attention-ordinal (via `config.full_attn_idxs()`), not by absolute
3621
+ * layer index.
3622
+ *
3623
+ * Opt out with `use_block_paged_cache: Some(false)` to revert to the
3624
+ * fully flat `Lfm2LayerCache` path on all layers.
3625
+ */
3626
+ useBlockPagedCache?: boolean | undefined;
3627
+ /**
3628
+ * MLP intermediate size for the DENSE-in-MoE layers (`layer_idx <
3629
+ * num_dense_layers`). Used DIRECTLY (no 2/3 `computed_ff_dim()` shrink).
3630
+ * Only present on MoE checkpoints.
3631
+ */
3632
+ intermediateSize?: number | undefined;
3633
+ /** Per-expert MLP intermediate size for the sparse MoE layers. */
3634
+ moeIntermediateSize?: number | undefined;
3635
+ /** Total number of routed experts. */
3636
+ numExperts?: number | undefined;
3637
+ /** Top-k experts selected per token. */
3638
+ numExpertsPerTok?: number | undefined;
3639
+ /** Number of leading DENSE layers before MoE layers begin. */
3640
+ numDenseLayers?: number | undefined;
3641
+ /**
3642
+ * Renormalize the top-k routing weights to sum to 1 (`/(sum+1e-20)`).
3643
+ *
3644
+ * `Option<bool>` so TS callers may omit it (napi renders bare `bool` as
3645
+ * required). Absent (None) is read as `true` everywhere via
3646
+ * `.unwrap_or(true)`, matching the prior `default = "default_true"`.
3647
+ */
3648
+ normTopkProb?: boolean | undefined;
3649
+ /**
3650
+ * Add the learned per-expert bias to the post-softmax gates BEFORE top-k.
3651
+ *
3652
+ * `Option<bool>` so TS callers may omit it (napi renders bare `bool` as
3653
+ * required). Absent (None) is read as `true` everywhere via
3654
+ * `.unwrap_or(true)`, matching the prior `default = "default_true"`.
3655
+ */
3656
+ useExpertBias?: boolean | undefined;
3359
3657
  }
3360
3658
 
3361
3659
  export interface MemorySnapshot {
@@ -3367,6 +3665,28 @@ export interface MemorySnapshot {
3367
3665
  cacheBytes: number;
3368
3666
  }
3369
3667
 
3668
+ /**
3669
+ * Return a snapshot of the MLX allocator's memory counters. Primarily
3670
+ * useful for dashboards and for debugging the `MLX_CACHE_LIMIT_GB`
3671
+ * override. Read-only — does not mutate allocator state.
3672
+ */
3673
+ export declare function memoryStats(): MemoryStats;
3674
+
3675
+ /**
3676
+ * Snapshot of the MLX Metal allocator's memory state. All values are in
3677
+ * bytes and returned as `f64` to avoid forcing BigInt round-trips in JS.
3678
+ */
3679
+ export interface MemoryStats {
3680
+ /** Actively-used memory (excludes the cached free-pool). */
3681
+ active: number;
3682
+ /** Peak memory usage since load / the last `resetPeakMemory`. */
3683
+ peak: number;
3684
+ /** Cache / free-pool memory currently held by the allocator. */
3685
+ cache: number;
3686
+ /** Metal `max_recommended_working_set_size` snapshot (0 on non-Metal). */
3687
+ wiredLimit: number;
3688
+ }
3689
+
3370
3690
  /** Full model configuration */
3371
3691
  export interface ModelConfig {
3372
3692
  visionConfig: VisionConfig;
@@ -3380,6 +3700,21 @@ export interface ModelConfig {
3380
3700
  eosTokenId: number;
3381
3701
  }
3382
3702
 
3703
+ /**
3704
+ * Ordering policy for structured multimodal content parts handed to a
3705
+ * checkpoint-provided Jinja template.
3706
+ *
3707
+ * The default preserves the generic serializer's existing text-before-media
3708
+ * behavior. PaddleOCR-VL and Qianfan-OCR were trained with image placeholders
3709
+ * before the instruction and opt into
3710
+ * [`MultimodalContentOrder::ImagesThenText`] at their adapter boundaries.
3711
+ * Audio remains after text in both modes.
3712
+ */
3713
+ export declare const enum MultimodalContentOrder {
3714
+ TextThenMedia = 'textThenMedia',
3715
+ ImagesThenText = 'imagesThenText',
3716
+ }
3717
+
3383
3718
  /** Result from document orientation classification. */
3384
3719
  export interface OrientationResult {
3385
3720
  /** Detected rotation angle (0, 90, 180, or 270 degrees) */
@@ -3410,56 +3745,6 @@ export interface OutputStoreConfig {
3410
3745
  localPath: string;
3411
3746
  }
3412
3747
 
3413
- /** Paged attention memory statistics (NAPI-compatible) */
3414
- export interface PagedCacheStats {
3415
- /** Total number of blocks in the pool */
3416
- totalBlocks: number;
3417
- /** Number of free blocks */
3418
- freeBlocks: number;
3419
- /** Number of allocated blocks */
3420
- allocatedBlocks: number;
3421
- /** Total memory in MB */
3422
- totalMemoryMb: number;
3423
- /** Used memory in MB */
3424
- usedMemoryMb: number;
3425
- /** Utilization percentage */
3426
- utilizationPercent: number;
3427
- }
3428
-
3429
- /** A completed sequence from paged generation */
3430
- export interface PagedCompletedSequence {
3431
- /** Original request ID */
3432
- requestId: string;
3433
- /** All generated tokens (excluding prompt) */
3434
- tokens: Array<number>;
3435
- /** Reason for completion ("stop", "length", "repetition", "tool_calls") */
3436
- finishReason: string;
3437
- }
3438
-
3439
- /** Result of a paged generation step */
3440
- export interface PagedGenerationStep {
3441
- /** Token outputs for each sequence in the batch */
3442
- outputs: Array<PagedTokenOutput>;
3443
- /** Number of sequences that were in prefill phase */
3444
- numPrefill: number;
3445
- /** Number of sequences that were in decode phase */
3446
- numDecode: number;
3447
- }
3448
-
3449
- /** Output from a single token generation step in paged attention */
3450
- export interface PagedTokenOutput {
3451
- /** Sequence ID in the scheduler */
3452
- seqId: number;
3453
- /** Request ID for this sequence */
3454
- requestId: string;
3455
- /** Generated token ID */
3456
- token: number;
3457
- /** Log probability of the token (f64 for NAPI compatibility) */
3458
- logprob: number;
3459
- /** Whether this sequence has finished */
3460
- isFinished: boolean;
3461
- }
3462
-
3463
3748
  /** A text paragraph */
3464
3749
  export interface Paragraph {
3465
3750
  content: string;
@@ -3541,6 +3826,52 @@ export interface PerformanceMetrics {
3541
3826
  * Excludes the first token (counted as prefill).
3542
3827
  */
3543
3828
  decodeTokensPerSecond: number;
3829
+ /**
3830
+ * MTP speculative decode: mean accepted *draft* tokens per cycle
3831
+ * (range `[0, depth]`). EXCLUDES the always-verified token each cycle
3832
+ * commits. `None` on plain autoregressive runs where no MTP cycle
3833
+ * executed. This is the historical drafts-only metric; for the
3834
+ * mlx-vlm-comparable headline see [`Self::mtp_mean_accepted_tokens_total`].
3835
+ */
3836
+ mtpMeanAcceptedTokens?: number;
3837
+ /**
3838
+ * MTP speculative decode: mean *committed* tokens per cycle, INCLUDING
3839
+ * the single always-verified token each cycle emits — i.e.
3840
+ * `mtp_accepted_drafts_total / mtp_cycles + 1.0`. This is the
3841
+ * mlx-vlm-comparable headline accept rate: it equals mlx-vlm's
3842
+ * `mean_accepted_tokens = (accepted_drafts + rounds) / rounds`
3843
+ * (`mlx-vlm/mlx_vlm/speculative/common.py:247`), where our
3844
+ * `mtp_cycles` is the 1:1 analog of mlx-vlm's `rounds` (one
3845
+ * draft+verify iteration; `record_mtp_cycle` is called exactly once
3846
+ * per cycle). The per-cycle `+1.0` matches mlx-vlm's `+rounds`
3847
+ * assumption — every round commits exactly one verified token
3848
+ * (the residual on partial-accept, the bonus on full-accept), even
3849
+ * when the final cycle's tail is EOS/length-truncated downstream
3850
+ * (mlx-vlm makes the same assumption: it appends to `accept_lens`
3851
+ * once per round regardless of truncation). `None` on plain
3852
+ * autoregressive runs.
3853
+ */
3854
+ mtpMeanAcceptedTokensTotal?: number;
3855
+ /**
3856
+ * MTP speculative decode: per-draft-position acceptance rate
3857
+ * (index = draft position). `None` on plain autoregressive runs.
3858
+ */
3859
+ mtpAcceptanceByPosition?: Array<number>;
3860
+ /**
3861
+ * MTP speculative decode: number of draft+verify cycles executed.
3862
+ * `None` on plain autoregressive runs.
3863
+ */
3864
+ mtpCycles?: number;
3865
+ /**
3866
+ * MTP speculative decode: mean attempted draft depth per cycle.
3867
+ * `None` on plain autoregressive runs.
3868
+ */
3869
+ mtpMeanDepth?: number;
3870
+ /**
3871
+ * Optional decode phase breakdown. Present when decode profiling
3872
+ * is enabled via `MLX_PROFILE_DECODE=1` or `setProfilingEnabled(true)`.
3873
+ */
3874
+ profilePhases?: Array<PhaseProfile>;
3544
3875
  }
3545
3876
 
3546
3877
  export interface PhaseProfile {
@@ -3554,6 +3885,76 @@ export interface PhaseProfile {
3554
3885
  count: number;
3555
3886
  }
3556
3887
 
3888
+ /**
3889
+ * Per-call Viterbi calibration overrides.
3890
+ *
3891
+ * Any field set to `Some(_)` overrides the corresponding bias from the
3892
+ * model's default calibration (loaded from `viterbi_calibration.json`
3893
+ * at load time). Missing fields fall back to the default.
3894
+ */
3895
+ export interface PrivacyCalibration {
3896
+ transitionBiasBackgroundStay?: number;
3897
+ transitionBiasBackgroundToStart?: number;
3898
+ transitionBiasEndToBackground?: number;
3899
+ transitionBiasEndToStart?: number;
3900
+ transitionBiasInsideToContinue?: number;
3901
+ transitionBiasInsideToEnd?: number;
3902
+ }
3903
+
3904
+ /**
3905
+ * Options for [`PrivacyFilterModelJs::classify`].
3906
+ *
3907
+ * - `threshold` (default `0.5`): minimum mean per-token probability for
3908
+ * an extracted span to be returned.
3909
+ * - `calibration`: per-call overrides on top of the checkpoint default.
3910
+ * - `return_tokens` (default `false`): when `true`, the result includes
3911
+ * a `tokens` array with one entry per input token.
3912
+ */
3913
+ export interface PrivacyClassifyOptions {
3914
+ threshold?: number;
3915
+ calibration?: PrivacyCalibration;
3916
+ returnTokens?: boolean;
3917
+ }
3918
+
3919
+ /** Result of [`PrivacyFilterModelJs::classify`]. */
3920
+ export interface PrivacyClassifyResult {
3921
+ entities: Array<PrivacyEntity>;
3922
+ tokens?: Array<PrivacyToken>;
3923
+ }
3924
+
3925
+ /**
3926
+ * A privacy entity detected by [`PrivacyFilterModelJs::classify`].
3927
+ *
3928
+ * `start`/`end` are byte offsets into the input string (Hugging Face
3929
+ * `tokenizers` convention). `label` is the privacy class without the
3930
+ * BIOES prefix (e.g. `"private_email"`). `score` is the mean — across
3931
+ * the span's tokens — of the softmax probability of the Viterbi-emitted
3932
+ * tag at each token.
3933
+ */
3934
+ export interface PrivacyEntity {
3935
+ label: string;
3936
+ start: number;
3937
+ end: number;
3938
+ score: number;
3939
+ text: string;
3940
+ }
3941
+
3942
+ /**
3943
+ * Per-token output emitted when [`PrivacyClassifyOptions::return_tokens`]
3944
+ * is `true`. `tag` is the full BIOES tag (`"O"` or `"B-..."`/`"I-..."`/
3945
+ * `"E-..."`/`"S-..."`) chosen by the Viterbi decoder. `score` is the
3946
+ * softmax probability of that emitted tag at this token, so `tag` and
3947
+ * `score` always share decoders (at boundary tokens the Viterbi tag can
3948
+ * differ from the local argmax).
3949
+ */
3950
+ export interface PrivacyToken {
3951
+ text: string;
3952
+ tag: string;
3953
+ score: number;
3954
+ start: number;
3955
+ end: number;
3956
+ }
3957
+
3557
3958
  export interface ProfilingSession {
3558
3959
  /** GPU hardware info. */
3559
3960
  gpuInfo: GpuInfo;
@@ -3601,6 +4002,32 @@ export interface QianfanOcrConfig {
3601
4002
  minDynamicPatch: number;
3602
4003
  }
3603
4004
 
4005
+ /** Microbench result for the production quantized qmv dispatch path. */
4006
+ export interface QmvQuantizedMicrobenchResult {
4007
+ /** Median `quantized_matmul` wall-clock per call, in nanoseconds. */
4008
+ medianNs: number;
4009
+ /** A tiny materialized checksum of the final output, used to keep the call live. */
4010
+ checksum: number;
4011
+ }
4012
+
4013
+ /**
4014
+ * Run the production quantized-qmv microbench in the current process.
4015
+ *
4016
+ * To compare `MLX_MTP_SMALL_M_QMV=0` versus `1`, call this from separate
4017
+ * processes. MLX caches the env-backed dispatch predicate statically.
4018
+ */
4019
+ export declare function quantizedQmvMicrobench(
4020
+ k: number,
4021
+ n: number,
4022
+ m: number,
4023
+ groupSize: number,
4024
+ bits: number,
4025
+ mode: string,
4026
+ dtype: DType,
4027
+ warmup?: number | undefined | null,
4028
+ iters?: number | undefined | null,
4029
+ ): QmvQuantizedMicrobenchResult;
4030
+
3604
4031
  /**
3605
4032
  * Qwen3.5 model configuration (dense variant).
3606
4033
  *
@@ -3629,6 +4056,79 @@ export interface Qwen35Config {
3629
4056
  fullAttentionInterval: number;
3630
4057
  partialRotaryFactor: number;
3631
4058
  ropeTheta: number;
4059
+ /**
4060
+ * GPU memory budget for paged KV cache in megabytes.
4061
+ * Only used when `use_block_paged_cache` is true.
4062
+ * Default: automatically sized for one full-context sequence.
4063
+ */
4064
+ pagedCacheMemoryMb?: number | undefined;
4065
+ /**
4066
+ * Block size for paged attention (tokens per block).
4067
+ * Only used when `use_block_paged_cache` is true.
4068
+ * Default: 16.
4069
+ */
4070
+ pagedBlockSize?: number | undefined;
4071
+ /**
4072
+ * Use the block-paged KV cache adapter (`PagedKVCacheAdapter`) for
4073
+ * full-attention layers.
4074
+ *
4075
+ * **OPT-IN — experimental.** When `Some(true)`, `Qwen35Inner`
4076
+ * allocates a `BlockAllocator` + `LayerKVPool` pair sized for the
4077
+ * model's full-attention layer count and constructs a
4078
+ * `PagedKVCacheAdapter`. The chat-session forward dispatch routes
4079
+ * full-attention layers through this adapter while linear-attention
4080
+ * (GatedDeltaNet / GDN) layers continue to use the existing
4081
+ * `Qwen3_5LayerCache::Linear(ArraysCache)` path with no
4082
+ * cross-request prefix reuse — vLLM's `MambaManager`-style "no
4083
+ * prefix reuse for recurrent layers" stance.
4084
+ *
4085
+ * **Paged vs flat eager**: this flag selects the eager paged decode
4086
+ * over the eager flat decode. When `Some(true)`, full-attention
4087
+ * layers run through the paged adapter (cross-request prefix reuse);
4088
+ * when unset, they run the eager flat decode. Either way the forward
4089
+ * is pure-Rust eager.
4090
+ *
4091
+ * **VLM under paged**: a VLM checkpoint defaults this flag ON at load, so
4092
+ * dense image turns ONLY run on the paged-vision core. A fresh single-turn
4093
+ * image-bearing prompt prefills through the paged adapter (M-RoPE positions
4094
+ * feed the rotary; the merged vision embeddings feed the forward) and
4095
+ * decodes plain AR — MTP weights are ignored on image turns. Warm
4096
+ * image-bearing session continues / cache-hit reuse are still rejected at
4097
+ * runtime (the GDN two-pass warm prefix is not byte-exact). A vision turn
4098
+ * that reaches a None adapter (explicit `Some(false)`, non-Metal build, or
4099
+ * a sym8 checkpoint) errors at dispatch.
4100
+ *
4101
+ * Default: `None` for text-only checkpoints (eager flat decode);
4102
+ * `Some(true)` for VLM checkpoints (block-paged, set in `parse_config`).
4103
+ */
4104
+ useBlockPagedCache?: boolean | undefined;
4105
+ /**
4106
+ * Persist the out-of-pool GDN recurrent state (and the paged KV blocks it
4107
+ * gates) to the SSD cold tier so warm prefixes survive process restarts.
4108
+ * Off unless explicitly enabled. See `crate::models::qwen3_5::gdn_sidecar`
4109
+ * and `crate::cold_tier::resolve_persist_cold`.
4110
+ */
4111
+ persistPagedCache?: boolean | undefined;
4112
+ /**
4113
+ * Number of MTP (Multi-Token Prediction) head layers shipped with the
4114
+ * checkpoint. Populated from `mtp_num_hidden_layers` /
4115
+ * `num_nextn_predict_layers` in `config.json`. `0` means the
4116
+ * checkpoint has no MTP heads and the speculative-decode path is
4117
+ * unavailable.
4118
+ */
4119
+ nMtpLayers: number;
4120
+ }
4121
+
4122
+ /**
4123
+ * Trained and physically available active-context limits for one loaded
4124
+ * Qwen3.5 model. Values are snapshots because the physical pool is fixed for
4125
+ * the lifetime of the resident model.
4126
+ */
4127
+ export interface Qwen35ContextLimits {
4128
+ trainedWindowTokens: number;
4129
+ effectiveWindowTokens: number;
4130
+ pagedBlockCapacity: number;
4131
+ pagedBlockSize: number;
3632
4132
  }
3633
4133
 
3634
4134
  /** Generation configuration for Qwen3.5 */
@@ -3683,6 +4183,55 @@ export interface Qwen35MoeConfig {
3683
4183
  moeIntermediateSize?: number | undefined;
3684
4184
  normTopkProb: boolean;
3685
4185
  mlpOnlyLayers?: number[] | undefined;
4186
+ /**
4187
+ * GPU memory budget for paged KV cache in megabytes.
4188
+ * Only used when `use_block_paged_cache` is true.
4189
+ * Default: automatically sized for one full-context sequence.
4190
+ */
4191
+ pagedCacheMemoryMb?: number | undefined;
4192
+ /**
4193
+ * Block size for paged attention (tokens per block).
4194
+ * Only used when `use_block_paged_cache` is true.
4195
+ * Default: 16.
4196
+ */
4197
+ pagedBlockSize?: number | undefined;
4198
+ /**
4199
+ * Use the block-paged KV cache adapter for full-attention layers.
4200
+ *
4201
+ * **OPT-IN — experimental.** Same semantics as the dense
4202
+ * `Qwen3_5Config::use_block_paged_cache` field. Selects the eager
4203
+ * paged decode over the eager flat decode: routes full-attention
4204
+ * layers through `PagedKVCacheAdapter` (cross-request prefix reuse);
4205
+ * GDN linear-attention layers stay on `Qwen3_5LayerCache::Linear`
4206
+ * either way. When disabled, full-attention layers run the eager flat
4207
+ * decode instead.
4208
+ *
4209
+ * **VLM under paged**: a VLM checkpoint loads with this flag set, and a
4210
+ * fresh single-turn image-bearing prompt prefills through the paged
4211
+ * adapter (M-RoPE positions feed the rotary; the merged vision embeddings
4212
+ * feed the forward). Image-bearing MTP turns are still rejected at
4213
+ * runtime; warm image-bearing session continues / cache-hit reuse are
4214
+ * cold-started (no warm GDN two-pass prefix).
4215
+ *
4216
+ * Default: `None` / `false`.
4217
+ */
4218
+ useBlockPagedCache?: boolean | undefined;
4219
+ /**
4220
+ * Persist the out-of-pool GDN recurrent state (and the paged KV blocks it
4221
+ * gates) to the SSD cold tier so warm prefixes survive process restarts.
4222
+ * Off unless explicitly enabled. Shares the dense qwen3_5 GDN sidecar codec
4223
+ * (`crate::models::qwen3_5::gdn_sidecar`) via `to_dense_config`; the
4224
+ * precedence rules live in `crate::cold_tier::resolve_persist_cold`.
4225
+ */
4226
+ persistPagedCache?: boolean | undefined;
4227
+ /**
4228
+ * Number of MTP (Multi-Token Prediction) head layers shipped with
4229
+ * the checkpoint. Populated from `mtp_num_hidden_layers` /
4230
+ * `num_nextn_predict_layers` in `config.json`. `0` means the
4231
+ * checkpoint has no MTP heads and the speculative-decode path is
4232
+ * unavailable.
4233
+ */
4234
+ nMtpLayers: number;
3686
4235
  }
3687
4236
 
3688
4237
  /** Generation configuration for Qwen3.5 MoE */
@@ -3719,29 +4268,34 @@ export interface Qwen3Config {
3719
4268
  padTokenId: number;
3720
4269
  eosTokenId: number;
3721
4270
  bosTokenId: number;
3722
- /**
3723
- * Enable paged attention for memory-efficient inference.
3724
- * Default: false (use standard KVCache)
3725
- */
3726
- usePagedAttention?: boolean | undefined;
3727
4271
  /**
3728
4272
  * GPU memory budget for paged KV cache in megabytes.
3729
- * Only used when use_paged_attention is true.
3730
4273
  * Default: 2048 (2GB)
3731
4274
  */
3732
4275
  pagedCacheMemoryMb?: number | undefined;
3733
4276
  /**
3734
4277
  * Block size for paged attention (tokens per block).
3735
- * Only used when use_paged_attention is true.
3736
4278
  * Default: 16
3737
4279
  */
3738
4280
  pagedBlockSize?: number | undefined;
3739
4281
  /**
3740
- * Use FP8 cache for 2x memory reduction (experimental).
3741
- * Only used when use_paged_attention is true.
3742
- * Default: false
4282
+ * Use the block-paged KV cache adapter (`PagedKVCacheAdapter`).
4283
+ *
4284
+ * When `Some(true)` (the default for Qwen3), `Qwen3Inner` allocates a
4285
+ * `BlockAllocator` + `LayerKVPool` pair and constructs a
4286
+ * `PagedKVCacheAdapter` for cross-request KV prefix reuse (vLLM-style
4287
+ * block-paged storage with refcounted prefix caching). When
4288
+ * `Some(false)`, the flat (non-paged) `Vec<KVCache>` cache path is
4289
+ * used instead.
4290
+ *
4291
+ * Default: true.
4292
+ */
4293
+ useBlockPagedCache?: boolean | undefined;
4294
+ /**
4295
+ * Persist full paged KV blocks to the SSD cold tier so warm prefixes
4296
+ * survive process restarts. Off unless explicitly enabled.
3743
4297
  */
3744
- useFp8Cache?: boolean | undefined;
4298
+ persistPagedCache?: boolean | undefined;
3745
4299
  }
3746
4300
 
3747
4301
  /** Qwen3 language model configuration */
@@ -3768,6 +4322,14 @@ export interface RecResult {
3768
4322
  score: number;
3769
4323
  }
3770
4324
 
4325
+ /**
4326
+ * Reset MLX's peak-memory counter to the current active level.
4327
+ * Useful for measuring per-request peak memory in a long-running
4328
+ * process — call before a request, sample
4329
+ * `getMemorySnapshot().peakBytes` after.
4330
+ */
4331
+ export declare function resetPeakMemory(): void;
4332
+
3771
4333
  /** Clear all collected profiling data and reset session timer. */
3772
4334
  export declare function resetProfilingData(): void;
3773
4335
 
@@ -3857,22 +4419,6 @@ export interface SamplingConfig {
3857
4419
  */
3858
4420
  export declare function saveToXlsx(text: string, filePath: string): void;
3859
4421
 
3860
- /** Scheduler statistics (NAPI-compatible) */
3861
- export interface SchedulerStatsNapi {
3862
- /** Number of requests waiting to be scheduled */
3863
- numWaiting: number;
3864
- /** Number of sequences currently running */
3865
- numRunning: number;
3866
- /** Number of completed sequences */
3867
- numCompleted: number;
3868
- /** Number of sequences in prefill phase */
3869
- numPrefill: number;
3870
- /** Number of sequences in decode phase */
3871
- numDecode: number;
3872
- /** Total tokens across all running sequences */
3873
- totalRunningTokens: number;
3874
- }
3875
-
3876
4422
  /** Enable or disable profiling globally. */
3877
4423
  export declare function setProfilingEnabled(enabled: boolean): void;
3878
4424
 
@@ -4155,6 +4701,35 @@ export interface TrainStepResultWithOutputs {
4155
4701
  completionLengths: Array<number>;
4156
4702
  }
4157
4703
 
4704
+ /**
4705
+ * Encoder-free vision configuration for the Gemma 4 unified multimodal model.
4706
+ *
4707
+ * Parsed from the `vision_config` sub-dict of a `gemma4_unified` checkpoint
4708
+ * (`model_type == "gemma4_unified_vision"`). This is a different shape from the
4709
+ * SigLIP-style [`super::vision_config::Gemma4VisionConfig`] used by the dense
4710
+ * gemma4 family: the unified vision path has no transformer encoder, only a
4711
+ * patch embedder (LayerNorm + Linear + 2D positional embedding) feeding the
4712
+ * multimodal projection.
4713
+ */
4714
+ export interface UnifiedVisionConfig {
4715
+ /** Pixel side length of a single image patch (48 = patch_size 16 × pooling 3). */
4716
+ modelPatchSize: number;
4717
+ /** Embedding width inside the vision embedder (3840, == text hidden_size). */
4718
+ mmEmbedDim: number;
4719
+ /** Number of rows in the 2D positional-embedding table (1120). */
4720
+ mmPosembSize: number;
4721
+ /** Maximum soft tokens (patches) per image after resize (280). */
4722
+ numSoftTokens: number;
4723
+ /** Output projection width of `embed_vision` (3840, == text hidden_size). */
4724
+ outputProjDims: number;
4725
+ /** Pixel-grid patch size used by the resize math (16). */
4726
+ patchSize: number;
4727
+ /** Pooling kernel size used by the resize math (3). */
4728
+ poolingKernelSize: number;
4729
+ /** Epsilon for the embedder LayerNorms and the projection RMSNorm. */
4730
+ rmsNormEps: number;
4731
+ }
4732
+
4158
4733
  /** Result from document unwarping. */
4159
4734
  export interface UnwarpResult {
4160
4735
  /** Unwarped image as PNG bytes */
@@ -4177,50 +4752,55 @@ export interface VisionConfig {
4177
4752
  spatialMergeSize: number;
4178
4753
  }
4179
4754
 
4180
- /** A batch item for VLM batch inference */
4181
4755
  export interface VlmBatchItem {
4182
- /** Chat messages for this item */
4183
4756
  messages: Array<VlmChatMessage>;
4184
- /** Encoded image buffers for this item (one image per item for OCR) */
4185
4757
  images?: Array<Buffer>;
4186
4758
  }
4187
4759
 
4188
- /** Configuration for VLM chat */
4189
4760
  export interface VlmChatConfig {
4190
- /** Encoded image buffers to process (PNG/JPEG bytes) */
4191
4761
  images?: Array<Buffer>;
4192
- /** Maximum number of new tokens to generate (default: 512) */
4193
4762
  maxNewTokens?: number;
4194
- /** Sampling temperature (0 = greedy, higher = more random) (default: 0.0 for OCR) */
4195
4763
  temperature?: number;
4196
- /** Top-k sampling (default: 0) */
4197
4764
  topK?: number;
4198
- /** Top-p (nucleus) sampling (default: 1.0) */
4199
4765
  topP?: number;
4200
- /** Repetition penalty (default: 1.5) */
4201
4766
  repetitionPenalty?: number;
4202
- /**
4203
- * Presence penalty (0.0 = disabled). Subtracts a flat penalty from logits of any
4204
- * token that appeared at least once in context. Matches OpenAI API semantics.
4205
- */
4206
4767
  presencePenalty?: number;
4207
- /** Number of recent tokens to consider for presence penalty (default: 20) */
4208
4768
  presenceContextSize?: number;
4209
- /**
4210
- * Frequency penalty (0.0 = disabled). Subtracts penalty * occurrence_count from
4211
- * logits of each token in context. Matches OpenAI API semantics.
4212
- */
4213
4769
  frequencyPenalty?: number;
4214
- /** Number of recent tokens to consider for frequency penalty (default: 20) */
4215
4770
  frequencyContextSize?: number;
4216
- /** Whether to return log probabilities (default: false) */
4217
4771
  returnLogprobs?: boolean;
4218
4772
  }
4219
4773
 
4220
- /** A chat message with optional image */
4774
+ /**
4775
+ * A chat message with textual content. Images are supplied through
4776
+ * [`VLMChatConfig`] and attached to the first user content part before the
4777
+ * model template is rendered.
4778
+ */
4221
4779
  export interface VlmChatMessage {
4222
- /** Role of the message sender */
4223
4780
  role: ChatRole;
4224
- /** Text content of the message */
4225
4781
  content: string;
4226
4782
  }
4783
+
4784
+ export declare namespace __internal__ {
4785
+ /**
4786
+ * Drain the MLX allocator's free-pool.
4787
+ *
4788
+ * @internal
4789
+ *
4790
+ * This is a process-wide drain routed through MLX's default-stream
4791
+ * `mlx_synchronize()`, which does NOT wait on the custom generation
4792
+ * streams that the per-model threads run on. Calling this from user
4793
+ * code while a decode is in flight can race live Metal command buffers
4794
+ * and risk use-after-free. The only safe caller today is
4795
+ * `@mlx-node/server`'s idle sweeper, which only triggers after the
4796
+ * in-flight request counter has returned to zero.
4797
+ *
4798
+ * Exposed under the `__internal__` NAPI namespace — reachable as
4799
+ * `require('@mlx-node/core').__internal__.clearCache()` and NOT on
4800
+ * the root `require('@mlx-node/core')` object. The namespace prefix
4801
+ * is a deliberate speed-bump that forces any caller to acknowledge
4802
+ * this is a private drain with custom-stream caveats; the root
4803
+ * surface stays clean of the footgun.
4804
+ */
4805
+ export function clearCache(): void;
4806
+ }