@mlx-node/core 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.cjs +85 -69
  2. package/index.d.cts +1321 -724
  3. package/package.json +6 -5
package/index.d.cts CHANGED
@@ -119,81 +119,125 @@ 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
+ * Runs the full jinja chat template once, decodes until the
199
+ * family's session stop token, and leaves the KV caches on a
200
+ * clean turn boundary so subsequent `chatSessionContinue` /
201
+ * `chatSessionContinueTool` calls can append a raw delta on
202
+ * top without re-rendering the chat template.
148
203
  */
149
204
  chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
150
205
  /**
151
206
  * Continue an existing chat session with a new user message.
152
207
  *
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.
208
+ * Appends a raw user/assistant delta to the session's cached
209
+ * KV state, then decodes the assistant reply, stopping on the
210
+ * family's session boundary token.
156
211
  *
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
212
+ * `images` is an opt-in guard parameter: when non-empty the
213
+ * native side returns an error whose message begins with
163
214
  * `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.
215
+ * `ChatSession` layer can route image-changes back through a
216
+ * fresh `chatSessionStart`.
167
217
  */
168
218
  chatSessionContinue(
169
219
  userMessage: string,
170
220
  images: Uint8Array[] | null | undefined,
221
+ audio: Uint8Array[] | null | undefined,
171
222
  config: ChatConfig | null | undefined,
172
223
  ): Promise<ChatResult>;
173
224
  /**
174
225
  * Continue an existing chat session with a tool-result turn.
175
226
  *
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.
227
+ * Builds the family's tool-result delta from `content` and
228
+ * prefills it on top of the live session caches, then decodes
229
+ * the assistant reply.
190
230
  *
191
- * Requires a live session started via `chatSessionStart`.
231
+ * `is_error` is the structured tool-error signal. When
232
+ * `Some(true)`, the renderer prepends the shared
233
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the
234
+ * rendered tool block.
192
235
  */
193
236
  chatSessionContinueTool(
194
237
  toolCallId: string,
195
238
  content: string,
196
239
  config?: ChatConfig | undefined | null,
240
+ isError?: boolean | undefined | null,
197
241
  ): Promise<ChatResult>;
198
242
  /** Streaming variant of `chatSessionStart`. */
199
243
  chatStreamSessionStart(
@@ -205,15 +249,24 @@ export declare class Gemma4Model {
205
249
  chatStreamSessionContinue(
206
250
  userMessage: string,
207
251
  images: Uint8Array[] | null | undefined,
252
+ audio: Uint8Array[] | null | undefined,
208
253
  config: ChatConfig | null | undefined,
209
254
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
210
255
  ): Promise<ChatStreamHandle>;
211
- /** Streaming variant of `chatSessionContinueTool`. */
256
+ /**
257
+ * Streaming variant of `chatSessionContinueTool`.
258
+ *
259
+ * `is_error` mirrors the non-streaming entry point — when
260
+ * `Some(true)`, the renderer prepends the shared
261
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the rendered
262
+ * tool block.
263
+ */
212
264
  chatStreamSessionContinueTool(
213
265
  toolCallId: string,
214
266
  content: string,
215
267
  config: ChatConfig | null | undefined,
216
268
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
269
+ isError?: boolean | null | undefined,
217
270
  ): Promise<ChatStreamHandle>;
218
271
  }
219
272
 
@@ -471,68 +524,78 @@ export declare class Lfm2Model {
471
524
  /** Load an LFM2 model from a directory containing safetensors and config.json. */
472
525
  static load(modelPath: string): Promise<Lfm2Model>;
473
526
  /**
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.
527
+ * Whether the block-paged KV cache adapter is active on this model
528
+ * instance.
529
+ *
530
+ * `true` iff `Lfm2Inner::paged_adapter` was successfully constructed
531
+ * at load time (driven by `Lfm2Config::use_block_paged_cache`,
532
+ * defaulting to `true` after paged-vs-flat parity verification).
533
+ * LFM2 is hybrid (10 conv + 6 full-attention layers); only the
534
+ * full-attention layers route through the adapter, conv layers stay
535
+ * on flat `Lfm2LayerCache::Conv` regardless. When `true`, the native
536
+ * cache reuses SYS blocks across `chatSessionStart` calls via
537
+ * content-addressing, so the JS-side warm slot in
538
+ * `SessionRegistry.getOrCreateWarmAny` is redundant and the
539
+ * `/v1/messages` server endpoint allocates a fresh `ChatSession` per
540
+ * request.
541
+ */
542
+ hasBlockPagedCache(): boolean;
543
+ /** Get the model configuration. */
544
+ getConfig(): Lfm2Config;
545
+ /** Estimated number of model parameters. */
546
+ numParameters(): number;
547
+ /**
548
+ * Reset all caches and clear cached token history. Exposed
549
+ * so tests and session-management code can start from a
550
+ * known clean state between turns.
477
551
  */
478
552
  resetCaches(): void;
479
553
  /**
480
554
  * Start a new chat session.
481
555
  *
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).
556
+ * Runs the full jinja chat template once, decodes until the
557
+ * family's session stop token, and leaves the KV caches on a
558
+ * clean turn boundary so subsequent `chatSessionContinue` /
559
+ * `chatSessionContinueTool` calls can append a raw delta on
560
+ * top without re-rendering the chat template.
489
561
  */
490
562
  chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
491
563
  /**
492
564
  * Continue an existing chat session with a new user message.
493
565
  *
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`.
566
+ * Appends a raw user/assistant delta to the session's cached
567
+ * KV state, then decodes the assistant reply, stopping on the
568
+ * family's session boundary token.
502
569
  *
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.
570
+ * `images` is an opt-in guard parameter: when non-empty the
571
+ * native side returns an error whose message begins with
572
+ * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
573
+ * `ChatSession` layer can route image-changes back through a
574
+ * fresh `chatSessionStart`.
509
575
  */
510
576
  chatSessionContinue(
511
577
  userMessage: string,
512
578
  images: Uint8Array[] | null | undefined,
579
+ audio: Uint8Array[] | null | undefined,
513
580
  config: ChatConfig | null | undefined,
514
581
  ): Promise<ChatResult>;
515
582
  /**
516
583
  * Continue an existing chat session with a tool-result turn.
517
584
  *
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
- */
585
+ * Builds the family's tool-result delta from `content` and
586
+ * prefills it on top of the live session caches, then decodes
587
+ * the assistant reply.
588
+ *
589
+ * `is_error` is the structured tool-error signal. When
590
+ * `Some(true)`, the renderer prepends the shared
591
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the
592
+ * rendered tool block.
593
+ */
532
594
  chatSessionContinueTool(
533
595
  toolCallId: string,
534
596
  content: string,
535
597
  config?: ChatConfig | undefined | null,
598
+ isError?: boolean | undefined | null,
536
599
  ): Promise<ChatResult>;
537
600
  /** Streaming variant of `chatSessionStart`. */
538
601
  chatStreamSessionStart(
@@ -544,20 +607,25 @@ export declare class Lfm2Model {
544
607
  chatStreamSessionContinue(
545
608
  userMessage: string,
546
609
  images: Uint8Array[] | null | undefined,
610
+ audio: Uint8Array[] | null | undefined,
547
611
  config: ChatConfig | null,
548
612
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
549
613
  ): Promise<ChatStreamHandle>;
550
- /** Streaming variant of `chatSessionContinueTool`. */
614
+ /**
615
+ * Streaming variant of `chatSessionContinueTool`.
616
+ *
617
+ * `is_error` mirrors the non-streaming entry point — when
618
+ * `Some(true)`, the renderer prepends the shared
619
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the rendered
620
+ * tool block.
621
+ */
551
622
  chatStreamSessionContinueTool(
552
623
  toolCallId: string,
553
624
  content: string,
554
625
  config: ChatConfig | null,
555
626
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
627
+ isError?: boolean | null | undefined,
556
628
  ): Promise<ChatStreamHandle>;
557
- /** Get the model configuration. */
558
- getConfig(): Lfm2Config;
559
- /** Estimated number of model parameters. */
560
- numParameters(): number;
561
629
  }
562
630
 
563
631
  export declare class MxArray {
@@ -681,9 +749,20 @@ export declare class MxArray {
681
749
  divScalar(value: number): MxArray;
682
750
  matmul(other: MxArray): MxArray;
683
751
  /**
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)
752
+ * Matrix multiply-add: D = beta * C + alpha * (self @ B), where self is A.
753
+ * Default: alpha=1.0, beta=1.0, giving D = C + (self @ B).
754
+ *
755
+ * Computed explicitly (matmul, optional alpha scale, then add beta*C)
756
+ * rather than via the fused `mlx::core::addmm` primitive. The fused
757
+ * primitive is correct on a well-formed metallib, but this project's local
758
+ * release build is known to non-deterministically miscompile fused GEMM
759
+ * kernels (see the metallib-corruption notes), which manifested as the
760
+ * fused addmm dropping `beta*C` and corrupting every biased linear — most
761
+ * visibly the vision tower (`qkv`, `proj`, `fc1`, `fc2`, merger all carry a
762
+ * bias; bias-free LM/MoE linears pass a zero `C` and were unaffected). The
763
+ * explicit form keeps the `C` term robust to that build hazard; the
764
+ * `nn::linear` unit tests assert it applies a non-zero `C`, so they double
765
+ * as a canary if a future build corrupts the matmul kernel too.
687
766
  */
688
767
  addmm(c: MxArray, b: MxArray, alpha?: number | undefined | null, beta?: number | undefined | null): MxArray;
689
768
  abs(): MxArray;
@@ -941,23 +1020,43 @@ export declare class OutputStore {
941
1020
  }
942
1021
 
943
1022
  /**
944
- * Opaque handle to KV cache state from a chat-session turn.
1023
+ * NAPI-exported view of [`PrivacyFilterModel`].
945
1024
  *
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
- *
950
- * Created internally by the model during chat-session turns.
951
- * Extract via `model.takeCache()`, restore via `model.setCache(cache)`.
1025
+ * Construct via [`PrivacyFilterModelJs::load`] and run end-to-end
1026
+ * classification via [`PrivacyFilterModelJs::classify`].
952
1027
  */
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;
1028
+ export declare class PrivacyFilterModel {
1029
+ /**
1030
+ * Load a privacy-filter checkpoint from a directory.
1031
+ *
1032
+ * The directory must contain `config.json`, `model.safetensors`,
1033
+ * `tokenizer.json`, and optionally `viterbi_calibration.json` and
1034
+ * `tokenizer_config.json`. Synchronous to match the loader pattern
1035
+ * used by every other model in this crate (e.g. `TextDetModel`).
1036
+ */
1037
+ static load(modelPath: string): PrivacyFilterModel;
1038
+ /**
1039
+ * Classify `text` and return detected PII entities (and optionally
1040
+ * per-token tags).
1041
+ *
1042
+ * Pipeline:
1043
+ * 1. Tokenize the text with byte offsets, no special tokens.
1044
+ * 2. Run the forward pass to get `[1, T, 33]` logits.
1045
+ * 3. Compute softmax (for per-tag confidences) and log-softmax (for
1046
+ * Viterbi emissions) over the class axis.
1047
+ * 4. Build the transition matrix from the default calibration
1048
+ * merged with any per-call overrides.
1049
+ * 5. Viterbi-decode using log-softmax emissions to get the BIOES
1050
+ * tag sequence.
1051
+ * 6. For each token, take the softmax probability of the
1052
+ * Viterbi-emitted tag (so per-token `tag` and `score` share
1053
+ * decoders), then walk the tags + offsets + those probabilities
1054
+ * to extract coherent spans whose mean probability clears
1055
+ * `threshold`.
1056
+ */
1057
+ classify(text: string, opts?: PrivacyClassifyOptions | undefined | null): PrivacyClassifyResult;
960
1058
  }
1059
+ export type PrivacyFilterModelJs = PrivacyFilterModel;
961
1060
 
962
1061
  /**
963
1062
  * Qianfan-OCR Vision-Language Model (InternVL architecture).
@@ -973,8 +1072,13 @@ export declare class QianfanOCRModel {
973
1072
  * Create a new QianfanOCRModel from config (uninitialized, no weights).
974
1073
  *
975
1074
  * 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.
1075
+ * instance is only useful for `is_initialized` queries until
1076
+ * [`QianfanOCRModel::load`] is called to actually run inference. The
1077
+ * `config` argument is accepted to preserve the `new
1078
+ * QianfanOCRModel(config)` JS surface; the value is discarded because
1079
+ * nothing on the uninitialized path consults it (any future config
1080
+ * getter would forward to the inner thread state populated by
1081
+ * `load()`).
978
1082
  */
979
1083
  constructor(config: QianfanOcrConfig);
980
1084
  /** Returns true if weights have been loaded via `load()`. */
@@ -1034,10 +1138,19 @@ export declare class QianfanOCRModel {
1034
1138
  * model backends. Qianfan-OCR is a VLM but the continue path cannot
1035
1139
  * splice new vision features into a live KV cache — image changes
1036
1140
  * always require a fresh session start.
1141
+ *
1142
+ * `audio` exists only to keep this method's positional ABI aligned
1143
+ * with the shared chat surface every other family exposes (the
1144
+ * `chat_napi_surface!` macro inserts `audio` between `images` and
1145
+ * `config`). Qianfan-OCR has no audio support, so a non-empty
1146
+ * `audio` is rejected at the boundary with the shared no-audio
1147
+ * error; `None` / empty is a complete no-op and audio is never
1148
+ * threaded into the model thread.
1037
1149
  */
1038
1150
  chatSessionContinue(
1039
1151
  userMessage: string,
1040
1152
  images: Uint8Array[] | null | undefined,
1153
+ audio: Uint8Array[] | null | undefined,
1041
1154
  config: ChatConfig | null | undefined,
1042
1155
  ): Promise<ChatResult>;
1043
1156
  /**
@@ -1048,12 +1161,20 @@ export declare class QianfanOCRModel {
1048
1161
  * decodes the model reply. Stops on `<|im_end|>` so the cache stays
1049
1162
  * on a clean turn boundary for the next turn.
1050
1163
  *
1164
+ * `is_error` is the structured tool-error signal. When `Some(true)`,
1165
+ * the renderer prepends the shared
1166
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the
1167
+ * `<tool_response>` wrapper so the model receives a clear text-level
1168
+ * cue. `None` / `Some(false)` keep the wire bytes byte-equal to the
1169
+ * pre-feature output.
1170
+ *
1051
1171
  * Requires a live session started via `chatSessionStart`.
1052
1172
  */
1053
1173
  chatSessionContinueTool(
1054
1174
  toolCallId: string,
1055
1175
  content: string,
1056
1176
  config?: ChatConfig | undefined | null,
1177
+ isError?: boolean | undefined | null,
1057
1178
  ): Promise<ChatResult>;
1058
1179
  /** Streaming variant of `chatSessionStart`. */
1059
1180
  chatStreamSessionStart(
@@ -1061,19 +1182,35 @@ export declare class QianfanOCRModel {
1061
1182
  config: ChatConfig | null | undefined,
1062
1183
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1063
1184
  ): Promise<ChatStreamHandle>;
1064
- /** Streaming variant of `chatSessionContinue`. */
1185
+ /**
1186
+ * Streaming variant of `chatSessionContinue`.
1187
+ *
1188
+ * `audio` mirrors the non-streaming entry point: it exists only to
1189
+ * keep the positional ABI aligned with the shared chat surface, and
1190
+ * a non-empty value is rejected at the boundary with the shared
1191
+ * no-audio error. `None` / empty is a complete no-op.
1192
+ */
1065
1193
  chatStreamSessionContinue(
1066
1194
  userMessage: string,
1067
1195
  images: Uint8Array[] | null | undefined,
1196
+ audio: Uint8Array[] | null | undefined,
1068
1197
  config: ChatConfig | null | undefined,
1069
1198
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1070
1199
  ): Promise<ChatStreamHandle>;
1071
- /** Streaming variant of `chatSessionContinueTool`. */
1200
+ /**
1201
+ * Streaming variant of `chatSessionContinueTool`.
1202
+ *
1203
+ * `is_error` mirrors the non-streaming entry point — when
1204
+ * `Some(true)`, the renderer prepends the shared
1205
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the
1206
+ * `<tool_response>` wrapper.
1207
+ */
1072
1208
  chatStreamSessionContinueTool(
1073
1209
  toolCallId: string,
1074
1210
  content: string,
1075
1211
  config: ChatConfig | null | undefined,
1076
1212
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1213
+ isError?: boolean | null | undefined,
1077
1214
  ): Promise<ChatStreamHandle>;
1078
1215
  }
1079
1216
 
@@ -1085,27 +1222,55 @@ export declare class QianfanOCRModel {
1085
1222
  * routed through `TrainingDispatch` to the model thread.
1086
1223
  */
1087
1224
  export declare class Qwen35Model {
1088
- /** Initialize caches for incremental generation. */
1089
- initCaches(): void;
1090
- /** Reset all caches. */
1091
- resetCaches(): void;
1092
1225
  /**
1093
- * Take the KV cache from the model, returning a `PromptCache` handle.
1226
+ * Whether the block-paged KV cache adapter is active on this model
1227
+ * instance.
1228
+ *
1229
+ * `true` iff `Qwen35Inner::paged_adapter` was successfully
1230
+ * constructed at load time (driven by
1231
+ * `Qwen3_5Config::use_block_paged_cache`, default-OFF for text-only
1232
+ * checkpoints because parity is pending real-weights validation, and
1233
+ * default-ON for VLM checkpoints). On VLM checkpoints dense image turns
1234
+ * ONLY run on the paged-vision core; a vision turn that reaches a None
1235
+ * adapter errors at dispatch. Surfaced through this NAPI method so
1236
+ * server endpoints can branch on it without round-tripping through
1237
+ * the model thread.
1238
+ */
1239
+ hasBlockPagedCache(): boolean;
1240
+ /**
1241
+ * Whether this checkpoint shipped an MTP head (module loaded by
1242
+ * `persistence::apply_weights_inner`). Snapshotted at load time from
1243
+ * `Qwen35Inner::has_mtp_weights()` so the TS `ChatSession` can
1244
+ * auto-default `enableMtp = true` for MTP-capable checkpoints without
1245
+ * dispatching a command into the model thread.
1246
+ *
1247
+ * Note: this only reports weight availability. Whether the
1248
+ * speculative-decode path actually runs on a given call also requires the
1249
+ * per-request `enableMtp` flag.
1250
+ */
1251
+ hasMtpWeights(): boolean;
1252
+ /**
1253
+ * Whether this loaded model instance can execute image-bearing turns.
1094
1254
  *
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.
1255
+ * This is an authoritative load-time snapshot, not a `config.json`
1256
+ * family guess: it requires a loaded vision encoder, image processor,
1257
+ * and the block-paged KV adapter used by the dense vision path.
1258
+ */
1259
+ supportsImages(): boolean;
1260
+ /**
1261
+ * Synchronous snapshot used by higher layers to preflight rendered
1262
+ * prompts and clamp output before native cache allocation.
1099
1263
  */
1100
- takeCache(): PromptCache | null;
1264
+ contextLimits(): Qwen35ContextLimits;
1101
1265
  /**
1102
- * Restore a previously taken `PromptCache` into the model.
1266
+ * Compute the exact prompt length after Qwen image-placeholder expansion
1267
+ * without running the vision encoder or touching inference caches.
1103
1268
  *
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.
1269
+ * `prompt_tokens` is the already-rendered chat-template output. `messages`
1270
+ * supplies the complete image history so both fresh and leased-session
1271
+ * preflights account for every image in template order.
1107
1272
  */
1108
- setCache(cache: PromptCache): void;
1273
+ expandedPromptTokenCount(promptTokens: Uint32Array, messages: Array<ChatMessage>): Promise<number>;
1109
1274
  /**
1110
1275
  * Load a pretrained model from a directory.
1111
1276
  *
@@ -1117,131 +1282,100 @@ export declare class Qwen35Model {
1117
1282
  static load(path: string): Promise<Qwen35Model>;
1118
1283
  /** Generate text from a prompt token sequence. */
1119
1284
  generate(promptTokens: MxArray, config: Qwen35GenerationConfig): Promise<Qwen35GenerationResult>;
1285
+ /**
1286
+ * Get the number of parameters in the model.
1287
+ *
1288
+ * Pure config computation — no model-thread dispatch needed.
1289
+ */
1290
+ numParameters(): number;
1291
+ /**
1292
+ * Save the model weights and configuration to a directory.
1293
+ *
1294
+ * Dispatches to model thread.
1295
+ */
1296
+ saveModel(savePath: string): Promise<undefined>;
1297
+ /**
1298
+ * Reset all caches and clear cached token history. Exposed
1299
+ * so tests and session-management code can start from a
1300
+ * known clean state between turns.
1301
+ */
1302
+ resetCaches(): void;
1120
1303
  /**
1121
1304
  * Start a new chat session.
1122
1305
  *
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.
1306
+ * Runs the full jinja chat template once, decodes until the
1307
+ * family's session stop token, and leaves the KV caches on a
1308
+ * clean turn boundary so subsequent `chatSessionContinue` /
1309
+ * `chatSessionContinueTool` calls can append a raw delta on
1310
+ * top without re-rendering the chat template.
1138
1311
  */
1139
1312
  chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1140
1313
  /**
1141
1314
  * Continue an existing chat session with a new user message.
1142
1315
  *
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`.
1316
+ * Appends a raw user/assistant delta to the session's cached
1317
+ * KV state, then decodes the assistant reply, stopping on the
1318
+ * family's session boundary token.
1150
1319
  *
1151
- * `images` is an opt-in guard parameter: when non-empty, the native
1152
- * side returns an error whose message begins with
1320
+ * `images` is an opt-in guard parameter: when non-empty the
1321
+ * native side returns an error whose message begins with
1153
1322
  * `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`.
1323
+ * `ChatSession` layer can route image-changes back through a
1324
+ * fresh `chatSessionStart`.
1156
1325
  */
1157
1326
  chatSessionContinue(
1158
1327
  userMessage: string,
1159
1328
  images: Uint8Array[] | null | undefined,
1329
+ audio: Uint8Array[] | null | undefined,
1160
1330
  config: ChatConfig | null | undefined,
1161
1331
  ): Promise<ChatResult>;
1162
1332
  /**
1163
1333
  * Continue an existing chat session with a tool-result turn.
1164
1334
  *
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.
1335
+ * Builds the family's tool-result delta from `content` and
1336
+ * prefills it on top of the live session caches, then decodes
1337
+ * the assistant reply.
1169
1338
  *
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`.
1339
+ * `is_error` is the structured tool-error signal. When
1340
+ * `Some(true)`, the renderer prepends the shared
1341
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the
1342
+ * rendered tool block.
1176
1343
  */
1177
1344
  chatSessionContinueTool(
1178
1345
  toolCallId: string,
1179
1346
  content: string,
1180
1347
  config?: ChatConfig | undefined | null,
1348
+ isError?: boolean | undefined | null,
1181
1349
  ): Promise<ChatResult>;
1182
- /**
1183
- * Streaming variant of `chatSessionStart`.
1184
- *
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.
1192
- */
1350
+ /** Streaming variant of `chatSessionStart`. */
1193
1351
  chatStreamSessionStart(
1194
1352
  messages: ChatMessage[],
1195
1353
  config: ChatConfig | null,
1196
1354
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1197
1355
  ): 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
- */
1356
+ /** Streaming variant of `chatSessionContinue`. */
1214
1357
  chatStreamSessionContinue(
1215
1358
  userMessage: string,
1216
1359
  images: Uint8Array[] | null | undefined,
1360
+ audio: Uint8Array[] | null | undefined,
1217
1361
  config: ChatConfig | null,
1218
1362
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1219
1363
  ): Promise<ChatStreamHandle>;
1220
1364
  /**
1221
1365
  * Streaming variant of `chatSessionContinueTool`.
1222
1366
  *
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`.
1367
+ * `is_error` mirrors the non-streaming entry point when
1368
+ * `Some(true)`, the renderer prepends the shared
1369
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the rendered
1370
+ * tool block.
1226
1371
  */
1227
1372
  chatStreamSessionContinueTool(
1228
1373
  toolCallId: string,
1229
1374
  content: string,
1230
1375
  config: ChatConfig | null,
1231
1376
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1377
+ isError?: boolean | null | undefined,
1232
1378
  ): 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
1379
  }
1246
1380
  export type Qwen3_5Model = Qwen35Model;
1247
1381
 
@@ -1253,142 +1387,147 @@ export type Qwen3_5Model = Qwen35Model;
1253
1387
  * routed through `TrainingDispatch` to the model thread.
1254
1388
  */
1255
1389
  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;
1390
+ /**
1391
+ * Whether the block-paged KV cache adapter is active on this model
1392
+ * instance.
1393
+ *
1394
+ * `true` iff `Qwen35MoeInner::paged_adapter` was successfully
1395
+ * constructed at load time (driven by
1396
+ * `Qwen3_5MoeConfig::use_block_paged_cache`, currently default-OFF
1397
+ * because parity is pending real-weights validation). On VLM
1398
+ * checkpoints the adapter can still be active for text-only
1399
+ * inference; image-bearing chat turns are rejected at runtime by
1400
+ * the chat-entry sites. Surfaced through this NAPI method so
1401
+ * server endpoints can branch on it without round-tripping through
1402
+ * the model thread.
1403
+ */
1404
+ hasBlockPagedCache(): boolean;
1405
+ /**
1406
+ * Whether this checkpoint shipped an MTP head (module loaded by
1407
+ * `persistence::apply_weights_moe_inner`). Snapshotted at load time from
1408
+ * `Qwen35MoeInner::has_mtp_weights()` so the TS `ChatSession` can
1409
+ * auto-default `enableMtp = true` for MTP-capable checkpoints without
1410
+ * dispatching a command into the model thread. Mirrors
1411
+ * `Qwen3_5Model::has_mtp_weights`.
1412
+ *
1413
+ * Note: this only reports weight availability. Whether the
1414
+ * speculative-decode path actually runs on a given call also requires
1415
+ * the per-request `enableMtp` flag.
1416
+ */
1417
+ hasMtpWeights(): boolean;
1418
+ /**
1419
+ * Whether this loaded model instance can execute image-bearing turns.
1420
+ *
1421
+ * This is an authoritative load-time snapshot, not a model-family guess:
1422
+ * it requires the loaded vision encoder, image processor, and block-paged
1423
+ * KV adapter used by the MoE vision path.
1424
+ */
1425
+ supportsImages(): boolean;
1426
+ /** Synchronous active-context snapshot shared with the dense wrapper. */
1427
+ contextLimits(): Qwen35ContextLimits;
1428
+ /**
1429
+ * Exact, non-mutating Qwen image-placeholder expansion count for a fully
1430
+ * rendered prompt and complete message history.
1431
+ */
1432
+ expandedPromptTokenCount(promptTokens: Uint32Array, messages: Array<ChatMessage>): Promise<number>;
1264
1433
  /** Load a pretrained model from a directory. */
1265
1434
  static load(path: string): Promise<Qwen35MoeModel>;
1266
1435
  /** Generate text from a prompt token sequence. */
1267
1436
  generate(promptTokens: MxArray, config: Qwen35MoeGenerationConfig): Promise<Qwen35MoeGenerationResult>;
1268
1437
  /**
1269
- * Start a new chat session.
1438
+ * Get the number of parameters in the model.
1270
1439
  *
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.
1440
+ * Pure config computation -- no model-thread dispatch needed.
1441
+ */
1442
+ numParameters(): number;
1443
+ /**
1444
+ * Save the model weights and configuration to a directory.
1276
1445
  *
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.
1446
+ * Dispatches to model thread.
1447
+ */
1448
+ saveModel(savePath: string): Promise<undefined>;
1449
+ /**
1450
+ * Reset all caches and clear cached token history. Exposed
1451
+ * so tests and session-management code can start from a
1452
+ * known clean state between turns.
1453
+ */
1454
+ resetCaches(): void;
1455
+ /**
1456
+ * Start a new chat session.
1283
1457
  *
1284
- * Requires `config.reuse_cache` to be enabled (the default).
1458
+ * Runs the full jinja chat template once, decodes until the
1459
+ * family's session stop token, and leaves the KV caches on a
1460
+ * clean turn boundary so subsequent `chatSessionContinue` /
1461
+ * `chatSessionContinueTool` calls can append a raw delta on
1462
+ * top without re-rendering the chat template.
1285
1463
  */
1286
1464
  chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1287
1465
  /**
1288
1466
  * Continue an existing chat session with a new user message.
1289
1467
  *
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`.
1468
+ * Appends a raw user/assistant delta to the session's cached
1469
+ * KV state, then decodes the assistant reply, stopping on the
1470
+ * family's session boundary token.
1297
1471
  *
1298
- * `images` is an opt-in guard parameter: when non-empty, the native
1299
- * side returns an error whose message begins with
1472
+ * `images` is an opt-in guard parameter: when non-empty the
1473
+ * native side returns an error whose message begins with
1300
1474
  * `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`.
1475
+ * `ChatSession` layer can route image-changes back through a
1476
+ * fresh `chatSessionStart`.
1303
1477
  */
1304
1478
  chatSessionContinue(
1305
1479
  userMessage: string,
1306
1480
  images: Uint8Array[] | null | undefined,
1481
+ audio: Uint8Array[] | null | undefined,
1307
1482
  config: ChatConfig | null | undefined,
1308
1483
  ): Promise<ChatResult>;
1309
1484
  /**
1310
1485
  * Continue an existing chat session with a tool-result turn.
1311
1486
  *
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.
1487
+ * Builds the family's tool-result delta from `content` and
1488
+ * prefills it on top of the live session caches, then decodes
1489
+ * the assistant reply.
1321
1490
  *
1322
- * Requires a live session started via `chatSessionStart`.
1491
+ * `is_error` is the structured tool-error signal. When
1492
+ * `Some(true)`, the renderer prepends the shared
1493
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the
1494
+ * rendered tool block.
1323
1495
  */
1324
1496
  chatSessionContinueTool(
1325
1497
  toolCallId: string,
1326
1498
  content: string,
1327
1499
  config?: ChatConfig | undefined | null,
1500
+ isError?: boolean | undefined | null,
1328
1501
  ): Promise<ChatResult>;
1329
- /**
1330
- * Streaming variant of `chatSessionStart`.
1331
- *
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.
1339
- */
1502
+ /** Streaming variant of `chatSessionStart`. */
1340
1503
  chatStreamSessionStart(
1341
1504
  messages: ChatMessage[],
1342
1505
  config: ChatConfig | null,
1343
1506
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1344
1507
  ): 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
- */
1508
+ /** Streaming variant of `chatSessionContinue`. */
1361
1509
  chatStreamSessionContinue(
1362
1510
  userMessage: string,
1363
1511
  images: Uint8Array[] | null | undefined,
1512
+ audio: Uint8Array[] | null | undefined,
1364
1513
  config: ChatConfig | null,
1365
1514
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1366
1515
  ): Promise<ChatStreamHandle>;
1367
1516
  /**
1368
1517
  * Streaming variant of `chatSessionContinueTool`.
1369
1518
  *
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`.
1519
+ * `is_error` mirrors the non-streaming entry point when
1520
+ * `Some(true)`, the renderer prepends the shared
1521
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the rendered
1522
+ * tool block.
1373
1523
  */
1374
1524
  chatStreamSessionContinueTool(
1375
1525
  toolCallId: string,
1376
1526
  content: string,
1377
1527
  config: ChatConfig | null,
1378
1528
  callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1529
+ isError?: boolean | null | undefined,
1379
1530
  ): 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
1531
  }
1393
1532
  export type Qwen3_5MoeModel = Qwen35MoeModel;
1394
1533
 
@@ -1400,117 +1539,21 @@ export type Qwen3_5MoeModel = Qwen35MoeModel;
1400
1539
  */
1401
1540
  export declare class Qwen3Model {
1402
1541
  /**
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;
1542
+ * Whether the block-paged KV cache adapter is active on this model
1543
+ * instance.
1544
+ *
1545
+ * `true` iff `Qwen3Inner::paged_adapter` was successfully constructed
1546
+ * at load time (driven by `Qwen3Config::use_block_paged_cache`,
1547
+ * defaulting to `true` for Qwen3 since paged-vs-flat parity has been
1548
+ * verified). When `true`, the native cache reuses SYS blocks across
1549
+ * `chatSessionStart` calls via content-addressing in
1550
+ * `BlockAllocator`'s prefix-hash table — the JS-side warm slot in
1551
+ * `SessionRegistry.getOrCreateWarmAny` becomes redundant and the
1552
+ * `/v1/messages` server endpoint allocates a fresh `ChatSession` per
1553
+ * request. See `packages/server/src/endpoints/messages.ts` for the
1554
+ * runtime-routing decision.
1555
+ */
1556
+ hasBlockPagedCache(): boolean;
1514
1557
  /** Get model configuration */
1515
1558
  getConfig(): Qwen3Config;
1516
1559
  /**
@@ -1544,83 +1587,6 @@ export declare class Qwen3Model {
1544
1587
  * ```
1545
1588
  */
1546
1589
  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
1590
  /**
1625
1591
  * Generate multiple completions for multiple prompts in batch
1626
1592
  *
@@ -1660,20 +1626,6 @@ export declare class Qwen3Model {
1660
1626
  groupSize: number,
1661
1627
  config?: GenerationConfig | undefined | null,
1662
1628
  ): 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
1629
  /**
1678
1630
  * Apply chat template and encode to token IDs
1679
1631
  *
@@ -1695,6 +1647,88 @@ export declare class Qwen3Model {
1695
1647
  tools?: Array<ToolDefinition> | undefined | null,
1696
1648
  enableThinking?: boolean | undefined | null,
1697
1649
  ): Promise<Uint32Array>;
1650
+ /**
1651
+ * Reset all caches and clear cached token history. Exposed
1652
+ * so tests and session-management code can start from a
1653
+ * known clean state between turns.
1654
+ */
1655
+ resetCaches(): void;
1656
+ /**
1657
+ * Start a new chat session.
1658
+ *
1659
+ * Runs the full jinja chat template once, decodes until the
1660
+ * family's session stop token, and leaves the KV caches on a
1661
+ * clean turn boundary so subsequent `chatSessionContinue` /
1662
+ * `chatSessionContinueTool` calls can append a raw delta on
1663
+ * top without re-rendering the chat template.
1664
+ */
1665
+ chatSessionStart(messages: Array<ChatMessage>, config?: ChatConfig | undefined | null): Promise<ChatResult>;
1666
+ /**
1667
+ * Continue an existing chat session with a new user message.
1668
+ *
1669
+ * Appends a raw user/assistant delta to the session's cached
1670
+ * KV state, then decodes the assistant reply, stopping on the
1671
+ * family's session boundary token.
1672
+ *
1673
+ * `images` is an opt-in guard parameter: when non-empty the
1674
+ * native side returns an error whose message begins with
1675
+ * `IMAGE_CHANGE_REQUIRES_SESSION_RESTART:` so the TypeScript
1676
+ * `ChatSession` layer can route image-changes back through a
1677
+ * fresh `chatSessionStart`.
1678
+ */
1679
+ chatSessionContinue(
1680
+ userMessage: string,
1681
+ images: Uint8Array[] | null | undefined,
1682
+ audio: Uint8Array[] | null | undefined,
1683
+ config: ChatConfig | null | undefined,
1684
+ ): Promise<ChatResult>;
1685
+ /**
1686
+ * Continue an existing chat session with a tool-result turn.
1687
+ *
1688
+ * Builds the family's tool-result delta from `content` and
1689
+ * prefills it on top of the live session caches, then decodes
1690
+ * the assistant reply.
1691
+ *
1692
+ * `is_error` is the structured tool-error signal. When
1693
+ * `Some(true)`, the renderer prepends the shared
1694
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the
1695
+ * rendered tool block.
1696
+ */
1697
+ chatSessionContinueTool(
1698
+ toolCallId: string,
1699
+ content: string,
1700
+ config?: ChatConfig | undefined | null,
1701
+ isError?: boolean | undefined | null,
1702
+ ): Promise<ChatResult>;
1703
+ /** Streaming variant of `chatSessionStart`. */
1704
+ chatStreamSessionStart(
1705
+ messages: ChatMessage[],
1706
+ config: ChatConfig | null,
1707
+ callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1708
+ ): Promise<ChatStreamHandle>;
1709
+ /** Streaming variant of `chatSessionContinue`. */
1710
+ chatStreamSessionContinue(
1711
+ userMessage: string,
1712
+ images: Uint8Array[] | null | undefined,
1713
+ audio: Uint8Array[] | null | undefined,
1714
+ config: ChatConfig | null,
1715
+ callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1716
+ ): Promise<ChatStreamHandle>;
1717
+ /**
1718
+ * Streaming variant of `chatSessionContinueTool`.
1719
+ *
1720
+ * `is_error` mirrors the non-streaming entry point — when
1721
+ * `Some(true)`, the renderer prepends the shared
1722
+ * [`crate::tokenizer::TOOL_ERROR_MARKER`] inside the rendered
1723
+ * tool block.
1724
+ */
1725
+ chatStreamSessionContinueTool(
1726
+ toolCallId: string,
1727
+ content: string,
1728
+ config: ChatConfig | null,
1729
+ callback: (err: Error | null, chunk: ChatStreamChunk) => void,
1730
+ isError?: boolean | null | undefined,
1731
+ ): Promise<ChatStreamHandle>;
1698
1732
  /**
1699
1733
  * Load a pretrained model from disk
1700
1734
  *
@@ -2167,124 +2201,46 @@ export declare class VLModel {
2167
2201
  * for loading a model from disk.
2168
2202
  */
2169
2203
  constructor(config: ModelConfig);
2170
- /** Set the tokenizer */
2171
- setTokenizer(tokenizer: Qwen3Tokenizer): void;
2172
- /** Check if tokenizer is available */
2173
- get hasTokenizer(): boolean;
2174
- /**
2175
- * Chat with the VLM model
2176
- *
2177
- * High-level API for conversational interaction with images.
2178
- *
2179
- * # Arguments
2180
- * * `messages` - Chat messages (role + content)
2181
- * * `config` - Chat configuration (including images for automatic processing)
2182
- *
2183
- * # Returns
2184
- * * VLMChatResult with generated text
2185
- *
2186
- * # Example
2187
- * ```typescript
2188
- * const result = await model.chat(
2189
- * [{ role: 'user', content: 'Describe this image.' }],
2190
- * { images: [readFileSync('./photo.jpg')], maxNewTokens: 256 }
2191
- * );
2192
- * ```
2193
- */
2194
- chat(messages: Array<VlmChatMessage>, config?: VlmChatConfig | undefined | null): Promise<VlmChatResult>;
2195
- /**
2196
- * Simple OCR: extract text from encoded image bytes
2197
- *
2198
- * Convenience method that processes an image and extracts all text.
2199
- *
2200
- * # Arguments
2201
- * * `image_data` - Encoded image bytes (PNG/JPEG)
2202
- * * `prompt` - Optional custom prompt (default: "Extract all text from this image.")
2203
- *
2204
- * # Returns
2205
- * * Extracted text as a string
2206
- *
2207
- * # Example
2208
- * ```typescript
2209
- * const text = await model.ocr(imageBuffer);
2210
- * console.log(text);
2211
- * ```
2212
- */
2213
- 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
2204
  /**
2249
- * Generate text tokens given input tokens and optional image
2205
+ * Chat with the VLM model
2250
2206
  *
2251
- * Uses KV caching for efficient generation.
2207
+ * High-level API for conversational interaction with images.
2252
2208
  *
2253
2209
  * # 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
2210
+ * * `messages` - Chat messages (role + content)
2211
+ * * `config` - Chat configuration (including images for automatic processing)
2258
2212
  *
2259
2213
  * # Returns
2260
- * * GenerationResult with tokens, logprobs, and finish reason
2214
+ * * VLMChatResult with generated text
2215
+ *
2216
+ * # Example
2217
+ * ```typescript
2218
+ * const result = await model.chat(
2219
+ * [{ role: 'user', content: 'Describe this image.' }],
2220
+ * { images: [readFileSync('./photo.jpg')], maxNewTokens: 256 }
2221
+ * );
2222
+ * ```
2261
2223
  */
2262
- generate(
2263
- inputIds: MxArray,
2264
- pixelValues?: MxArray | undefined | null,
2265
- imageGridThw?: MxArray | undefined | null,
2266
- config?: GenerationConfig | undefined | null,
2267
- ): Promise<GenerationResult>;
2224
+ chat(messages: Array<VlmChatMessage>, config?: VlmChatConfig | undefined | null): Promise<VlmChatResult>;
2268
2225
  /**
2269
- * Batch OCR: extract text from multiple images simultaneously
2226
+ * Simple OCR: extract text from encoded image bytes
2270
2227
  *
2271
- * Processes N images with sequential prefill + batched decode for ~N× decode throughput.
2228
+ * Convenience method that processes an image and extracts all text.
2272
2229
  *
2273
2230
  * # Arguments
2274
- * * `images` - Encoded image buffers
2275
- * * `config` - Optional chat configuration (shared across all items)
2231
+ * * `image_data` - Encoded image bytes (PNG/JPEG)
2232
+ * * `prompt` - Optional custom prompt (default: "Extract all text from this image.")
2276
2233
  *
2277
2234
  * # Returns
2278
- * * Vec of extracted text strings, one per image
2235
+ * * Extracted text as a string
2279
2236
  *
2280
2237
  * # Example
2281
2238
  * ```typescript
2282
- * import { readFileSync } from 'fs';
2283
- * const images = ['page1.jpg', 'page2.jpg'].map(p => readFileSync(p));
2284
- * const texts = await model.ocrBatch(images);
2239
+ * const text = await model.ocr(imageBuffer);
2240
+ * console.log(text);
2285
2241
  * ```
2286
2242
  */
2287
- ocrBatch(images: Array<Buffer>, config?: VlmChatConfig | undefined | null): Promise<Array<string>>;
2243
+ ocr(imageData: Buffer, prompt?: string | undefined | null): Promise<string>;
2288
2244
  /**
2289
2245
  * Batch chat: process multiple items simultaneously
2290
2246
  *
@@ -2323,25 +2279,6 @@ export declare class VLModel {
2323
2279
  * ```
2324
2280
  */
2325
2281
  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
2282
  }
2346
2283
 
2347
2284
  /**
@@ -2392,8 +2329,68 @@ export declare const enum BuiltinRewardType {
2392
2329
  JsonSchema = 'JsonSchema',
2393
2330
  }
2394
2331
 
2332
+ /**
2333
+ * Data-free static FP8 activation-amax calibration over RAW-text PREFILL
2334
+ * (NVIDIA modelopt `MaxCalibrator` parity), end to end in native code.
2335
+ *
2336
+ * The nvidia recipe covers BOTH `qwen3_5` (dense) and `qwen3_5_moe` (MoE), so
2337
+ * this reads `<model_path>/config.json`'s `model_type` and dispatches to the
2338
+ * matching loader + `CalibratePrefillRaw` command (any other `model_type` is a
2339
+ * clear error). Both loaders are the SAME ones the inference session uses
2340
+ * ([`persistence::load_with_thread`]) — the model is only usable on its
2341
+ * dedicated model thread. Then:
2342
+ * 1. dispatches `{Qwen35Cmd,Qwen35MoeCmd}::CalibratePrefillRaw`, which on the
2343
+ * model thread SELF-ARMS that thread's thread-local
2344
+ * [`ActivationAmaxCollector`] flag (RAII, AFTER load so no load-time eval
2345
+ * is recorded), tokenizes each `text` WITHOUT the chat template, truncates
2346
+ * to `calib_seq` tokens, and runs PREFILL ONLY (no generation) so every
2347
+ * mxfp8 attn/GDN projection's activation tap fires over realistic raw-text
2348
+ * activations, resetting caches between rows, then disarms on exit;
2349
+ * 2. ONLY if the full loop succeeded — drains the per-tensor amax and
2350
+ * ATOMICALLY writes it into `<model_path>/config.json` (temp file +
2351
+ * `rename`).
2352
+ *
2353
+ * CONCURRENCY: the whole clear→prefill→take→write section is serialized by
2354
+ * [`calib_guard`] (a process-wide `try_lock`); a second concurrent calibration
2355
+ * fails fast with "another calibration is in progress". The arm flag is
2356
+ * thread-local (so a concurrent inference model can't contaminate the run), but
2357
+ * the running-max MAP is process-global, so serializing RUNS keeps two
2358
+ * calibrations from interleaving `record`/`take` on it. The map is CLEARED at
2359
+ * the very start so stale amax from a prior PANICKED run cannot leak into this
2360
+ * write.
2361
+ *
2362
+ * On ANY error before the final write, the partial amax is discarded and
2363
+ * `config.json` is left UNTOUCHED (a failed calibration must not mutate the
2364
+ * live model in place). A run that prefilled ZERO rows (empty dataset, or every
2365
+ * row tokenized to nothing) is likewise an ERROR that leaves `config.json`
2366
+ * untouched — a no-op calibration must not report a silent success. Returns the
2367
+ * number of projections calibrated (the count of collected amax entries); 0
2368
+ * means a real prefill ran but the model exercised no activation-fp8 sites (not
2369
+ * an nvidia-recipe checkpoint), and in that case `config.json` is left
2370
+ * UNCHANGED (no rewrite).
2371
+ */
2372
+ export declare function calibrateActivationAmaxRaw(
2373
+ modelPath: string,
2374
+ texts: Array<string>,
2375
+ calibSeq: number,
2376
+ ): Promise<number>;
2377
+
2395
2378
  /** Unified chat configuration shared by all model variants (Qwen3, Qwen3.5, Qwen3.5 MoE). */
2396
2379
  export interface ChatConfig {
2380
+ /**
2381
+ * Internal logical cache owner. The agent provider forwards Pi's stable
2382
+ * session id so model-global GDN sidecars can retain parent and child
2383
+ * branches independently. This does not namespace the physical paged KV
2384
+ * cache; exact token/extra-key hashes remain shareable across owners.
2385
+ */
2386
+ cacheOwnerId?: string | undefined;
2387
+ /**
2388
+ * Internal top-level owner for the bounded Qwen3.5 GDN sidecar store.
2389
+ * `cache_owner_id` may identify a child Pi session; this separately
2390
+ * identifies the current interactive root so /new and /resume can rotate
2391
+ * the protected branch without changing PagedAttention cache identity.
2392
+ */
2393
+ cacheRootOwnerId?: string | undefined;
2397
2394
  maxNewTokens?: number | undefined;
2398
2395
  temperature?: number | undefined;
2399
2396
  topK?: number | undefined;
@@ -2417,11 +2414,11 @@ export interface ChatConfig {
2417
2414
  frequencyPenalty?: number | undefined;
2418
2415
  /** Number of recent tokens to consider for frequency penalty (default: 20) */
2419
2416
  frequencyContextSize?: number | undefined;
2420
- /** Max consecutive identical tokens before stopping (default: 16, 0 = disabled) */
2417
+ /** Max consecutive identical tokens before stopping (default: 0 = disabled; opt in with a positive value) */
2421
2418
  maxConsecutiveTokens?: number | undefined;
2422
- /** Max n-gram repetitions before stopping (default: 3, 0 = disabled) */
2419
+ /** Max n-gram repetitions before stopping (default: 0 = disabled; opt in with a positive value) */
2423
2420
  maxNgramRepeats?: number | undefined;
2424
- /** Max pattern size for n-gram repetition detection (default: 64) */
2421
+ /** Max pattern size for n-gram repetition detection (default: 0 = disabled; opt in with a positive value) */
2425
2422
  ngramSize?: number | undefined;
2426
2423
  tools?: Array<ToolDefinition>;
2427
2424
  /**
@@ -2454,6 +2451,56 @@ export interface ChatConfig {
2454
2451
  * avoiding redundant computation for multi-turn conversations.
2455
2452
  */
2456
2453
  reuseCache?: boolean | undefined;
2454
+ /**
2455
+ * MTP: opt-in flag enabling the Multi-Token Prediction speculative decode
2456
+ * loop (pure-Rust eager; qwen3.5 dense and MoE). Requires the model
2457
+ * checkpoint to carry an MTP head (otherwise silently ignored). Default:
2458
+ * `false`.
2459
+ */
2460
+ enableMtp?: boolean | undefined;
2461
+ /**
2462
+ * MTP: number of draft tokens per speculative cycle.
2463
+ *
2464
+ * On Qwen3.5 native MTP heads it is clamped to `[1, 5]` by the verify
2465
+ * FFI contract, and when unset native code currently pins depth 1.
2466
+ * When `mtpAdaptiveDepth` is `true`, this value is used as the
2467
+ * throughput-policy seed and the expected-value policy's max depth.
2468
+ * Adaptive depth is opt-in; set `mtpAdaptiveDepth: true` explicitly to
2469
+ * enable it.
2470
+ *
2471
+ * Gemma4 external drafts (`draftModelPath`) resolve the field per draft
2472
+ * variant instead (`gemma4/model.rs` `resolve_params`, always from the
2473
+ * RAW config value — the engine's central `[1, 5]` clamp is an MTP-head
2474
+ * contract that does not apply to external drafts):
2475
+ * - DSpark: with both knobs unset, full draft blocks (the checkpoint's
2476
+ * block size — 7 tokens on `dspark_gemma4_12b_block7`) run behind a
2477
+ * short target-AR/DSpark break-even calibration. A short generation
2478
+ * budget that cannot finish calibration retains the fixed-block
2479
+ * schedule. An explicit
2480
+ * `mtpDepth` caps and pins the block unless `mtpAdaptiveDepth: true`
2481
+ * opts the guard back in; explicit `false` disables it.
2482
+ * - Assistant (Google `gemma-4-*-it-assistant`): an unset `mtpDepth`
2483
+ * drafts 3 tokens per cycle (`ASSISTANT_DEFAULT_DEPTH`), and an
2484
+ * explicit `mtpDepth` clamps to `[1, 8]` (`ASSISTANT_MAX_DEPTH`).
2485
+ *
2486
+ * `mtpAdaptiveDepth` is ignored for the Gemma4 assistant variant.
2487
+ */
2488
+ mtpDepth?: number | undefined;
2489
+ /**
2490
+ * MTP: when true, the decode loop runs the adaptive
2491
+ * depth policy. Default mode is a per-depth EMA hill-climb plus
2492
+ * DFlash-style 3-state machine `full | reduced | probe`.
2493
+ * `MLX_MTP_ADAPTIVE_DEPTH_MODE=expected-value` instead uses the
2494
+ * MTPLX-style intra-cycle expected-value gate, which deepens toward
2495
+ * `mtpDepth` by default (T=0 byte-parity verified); set
2496
+ * `MLX_MTP_EV_ALLOW_DEEPEN=0` to pin the base depth.
2497
+ * When false, the loop pins `mtpDepth` for every cycle.
2498
+ *
2499
+ * Default: false, except Gemma4 DSpark enables its measured break-even
2500
+ * guard when both this field and `mtpDepth` are unset. An explicit value
2501
+ * always wins over the family default.
2502
+ */
2503
+ mtpAdaptiveDepth?: boolean | undefined;
2457
2504
  }
2458
2505
 
2459
2506
  /** Chat message with tool calling support */
@@ -2466,10 +2513,40 @@ export interface ChatMessage {
2466
2513
  toolCalls?: Array<ToolCall>;
2467
2514
  /** Tool call ID this message is responding to (for tool messages) */
2468
2515
  toolCallId?: string;
2516
+ /**
2517
+ * Whether this tool-role message represents an errored tool result.
2518
+ *
2519
+ * Authoritative, structured signal of tool-call failure. Set to
2520
+ * `Some(true)` when the caller (e.g. the Anthropic
2521
+ * `tool_result.is_error === true` translator) wants the model to
2522
+ * treat the tool output as an error. The renderer prepends a short
2523
+ * `[tool error]` prefix to `content` when emitting the wire-format
2524
+ * tool response so the model receives a clear text-level cue, but
2525
+ * the original `content` stays byte-for-byte intact in the
2526
+ * structured form — no JSON wrapping, no in-band marker that could
2527
+ * collide with a successful tool result whose literal content
2528
+ * happens to start with the same prefix.
2529
+ *
2530
+ * `None` / `Some(false)` produce the unmarked wire format.
2531
+ */
2532
+ isError?: boolean;
2469
2533
  /** Reasoning content for thinking mode (used with <think> tags) */
2470
2534
  reasoningContent?: string;
2535
+ /**
2536
+ * Thinking mode used when this assistant message was generated.
2537
+ *
2538
+ * This is replay provenance, not a request override. Gemma4's disabled-
2539
+ * thinking generation prefix contains an explicit empty thought channel,
2540
+ * while an enabled-thinking turn that emitted no reasoning contains no
2541
+ * such channel. Keeping the historical mode on the message lets the chat
2542
+ * template reproduce either byte sequence even when a later request
2543
+ * changes its current thinking setting.
2544
+ */
2545
+ thinkingEnabled?: boolean;
2471
2546
  /** Image data for VLM models (encoded image bytes: PNG/JPEG, passed as Uint8Array/Buffer) */
2472
2547
  images?: Array<Uint8Array> | undefined;
2548
+ /** Audio data for unified Gemma 4 (encoded audio bytes: WAV, passed as Uint8Array/Buffer) */
2549
+ audio?: Array<Uint8Array> | undefined;
2473
2550
  }
2474
2551
 
2475
2552
  /** Unified chat result shared by all model variants (Qwen3, Qwen3.5, Qwen3.5 MoE). */
@@ -2482,6 +2559,17 @@ export interface ChatResult {
2482
2559
  reasoningTokens: number;
2483
2560
  finishReason: string;
2484
2561
  rawText: string;
2562
+ /**
2563
+ * Number of prompt tokens served from the reused KV-cache prefix.
2564
+ *
2565
+ * When the native prefix-cache machinery successfully matches the new
2566
+ * prompt against the cached conversation history (via
2567
+ * `verify_cache_prefix_direct`), only the trailing delta is re-prefilled
2568
+ * and this field reports the length of the reused prefix. `0` when
2569
+ * the cache was missed or disabled and the full prompt had to be
2570
+ * re-prefilled.
2571
+ */
2572
+ cachedTokens: number;
2485
2573
  /** Performance metrics (present when `reportPerformance: true` in config) */
2486
2574
  performance?: PerformanceMetrics;
2487
2575
  }
@@ -2509,6 +2597,19 @@ export interface ChatStreamChunk {
2509
2597
  promptTokens?: number;
2510
2598
  reasoningTokens?: number;
2511
2599
  rawText?: string;
2600
+ /**
2601
+ * Number of prompt tokens served from the reused KV-cache prefix on
2602
+ * this turn. Populated on the terminal chunk (`done == true`) only;
2603
+ * `None` on mid-stream delta chunks.
2604
+ *
2605
+ * Zero on a cache miss or disabled reuse; equal to the matched
2606
+ * prefix length on a hit. Mirrors `ChatResult.cached_tokens`
2607
+ * verbatim so session-aware streaming consumers can observe
2608
+ * prefix-cache reuse without round-tripping to the non-streaming
2609
+ * path. Non-terminal chunks always carry `None` — only the
2610
+ * terminal chunk is authoritative.
2611
+ */
2612
+ cachedTokens?: number | undefined;
2512
2613
  /** Performance metrics (only present in the final chunk when `reportPerformance: true`) */
2513
2614
  performance?: PerformanceMetrics;
2514
2615
  /**
@@ -2579,7 +2680,11 @@ export interface ConversionOptions {
2579
2680
  quantBits?: number;
2580
2681
  /** Quantization group size (default: 64 for affine, 32 for mxfp8) */
2581
2682
  quantGroupSize?: number;
2582
- /** Quantization mode: "affine" (default) or "mxfp8" */
2683
+ /**
2684
+ * Quantization mode: "affine" (default), "mxfp4", "mxfp8", "nvfp4", or
2685
+ * "sym8" (per-output-channel symmetric int8; qwen3_5 + qwen3_5_moe + lfm2/lfm2_moe + gemma4,
2686
+ * implies bits=8, no group_size — consciously NOT mlx-lm-loadable)
2687
+ */
2583
2688
  quantMode?: string;
2584
2689
  /**
2585
2690
  * Quantization recipe for per-layer mixed-bit quantization.
@@ -2591,6 +2696,35 @@ export interface ConversionOptions {
2591
2696
  * Improves quantization quality by amplifying important weight channels.
2592
2697
  */
2593
2698
  imatrixPath?: string;
2699
+ /**
2700
+ * Upgrade quantization to micro-scaling FP (mxfp4 / mxfp8).
2701
+ * When true, applies after the recipe predicate: eligible 8-bit affine
2702
+ * decisions become mxfp8 and 4-bit become mxfp4. Kept affine (not upgraded):
2703
+ * affine-only loaders (lm_head, embed_tokens, router.proj,
2704
+ * embedding_projection) at their recipe bits, MoE router gates (8-bit affine),
2705
+ * and the recipe-pinned attention/GDN projections (o_proj / out_proj /
2706
+ * in_proj_a / in_proj_b, 8-bit affine). Requires `quant_mode = "affine"`.
2707
+ * Forces `group_size = 32` for upgraded layers.
2708
+ */
2709
+ quantMxfp?: boolean;
2710
+ /**
2711
+ * Optional Qwen MTP quantization policy: "off" (default), "cyankiwi", "all",
2712
+ * or "split" (alias "drafter").
2713
+ * "cyankiwi" keeps mtp.fc dense and quantizes only the MTP layer linears as
2714
+ * 4-bit affine group_size=32. For dense `qwen3_5` the quantized linears are
2715
+ * emitted into an MTPLX-compatible mtp.safetensors sidecar; for MoE
2716
+ * (`qwen3_5_moe`) there is no sidecar — they are quantized in place and stored
2717
+ * inline in the main safetensors shards.
2718
+ * "all" additionally quantizes mtp.fc. For dense `qwen3_5` the quantized MTP
2719
+ * linears land in the mtp.safetensors sidecar; for MoE (`qwen3_5_moe`) there
2720
+ * is no sidecar — they are quantized in place and stored inline in the main
2721
+ * safetensors shards.
2722
+ * "split"/"drafter" emits a body checkpoint with NO mtp.* tensors plus a
2723
+ * separate `mtp-drafter/` directory in mlx-vlm's `qwen3_5_mtp` format
2724
+ * (bare-keyed MTP head, format:mlx). It does NOT require --quantize/--q-recipe;
2725
+ * the body may be bf16 or already-quantized and the MTP head stays bf16.
2726
+ */
2727
+ quantMtp?: string;
2594
2728
  }
2595
2729
 
2596
2730
  export interface ConversionResult {
@@ -2662,11 +2796,11 @@ export declare function createRandomQwen35Checkpoint(config: Qwen35Config, saveP
2662
2796
  /**
2663
2797
  * Create a random-init Qwen3.5 MoE model and save it to disk.
2664
2798
  *
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
2799
+ * Spawns a dedicated model thread whose init runs
2800
+ * [`create_random_qwen35_moe_checkpoint_sync`] (random-init inner + save);
2801
+ * the thread holds no state and is dropped once the promise resolves, so
2802
+ * the in-memory model is released as soon as the checkpoint has been
2803
+ * written. Used by TypeScript test fixtures that need an on-disk
2670
2804
  * checkpoint without keeping a NAPI model instance alive.
2671
2805
  */
2672
2806
  export declare function createRandomQwen35MoeCheckpoint(config: Qwen35MoeConfig, savePath: string): Promise<undefined>;
@@ -2717,6 +2851,8 @@ export declare const enum DType {
2717
2851
  BFloat16 = 3,
2718
2852
  Uint32 = 4,
2719
2853
  Uint8 = 5,
2854
+ /** Signed int8 — sym8 per-output-channel symmetric quantized weights. */
2855
+ Int8 = 6,
2720
2856
  }
2721
2857
 
2722
2858
  /** Document element type */
@@ -2843,6 +2979,20 @@ export interface Gemma4Config {
2843
2979
  /** Head dimension for global layers. If None, uses head_dim. */
2844
2980
  globalHeadDim?: number;
2845
2981
  attentionKEqV: boolean;
2982
+ /**
2983
+ * True for the unified multimodal Gemma 4 checkpoint
2984
+ * (`model_type == "gemma4_unified"` or
2985
+ * `architectures[0] == "Gemma4UnifiedForConditionalGeneration"`).
2986
+ * The text decoder is shared, but the unified checkpoint carries
2987
+ * vision/audio embedder weights that must be dropped in a text-only load.
2988
+ */
2989
+ isUnified: boolean;
2990
+ /**
2991
+ * `text_config.use_bidirectional_attention` from the unified checkpoint
2992
+ * (e.g. `"vision"`). Parsed for a stable struct surface; the text-only
2993
+ * decode path does not consume it.
2994
+ */
2995
+ useBidirectionalAttention?: string;
2846
2996
  finalLogitSoftcapping?: number;
2847
2997
  perLayerInputEmbeds: boolean;
2848
2998
  hiddenSizePerLayerInput?: number;
@@ -2861,10 +3011,88 @@ export interface Gemma4Config {
2861
3011
  topKExperts?: number;
2862
3012
  moeIntermediateSize?: number;
2863
3013
  visionConfig?: Gemma4VisionConfig;
3014
+ /**
3015
+ * Encoder-free vision config for the unified multimodal checkpoint.
3016
+ * `Some` only when `is_unified` and the checkpoint carries a
3017
+ * `vision_config` sub-dict. Disjoint from `vision_config` (the SigLIP
3018
+ * path) — the unified vision embedder is built from this instead.
3019
+ */
3020
+ unifiedVisionConfig?: UnifiedVisionConfig;
2864
3021
  imageTokenId?: number;
2865
3022
  boiTokenId?: number;
2866
3023
  eoiTokenId?: number;
2867
3024
  visionSoftTokensPerImage?: number;
3025
+ /**
3026
+ * True when the checkpoint declares an `audio_config` sub-dict. Parallels
3027
+ * `unified_vision_config.is_some()`; gates the un-drop + load of
3028
+ * `embed_audio` weights and the audio merge path.
3029
+ */
3030
+ hasAudio: boolean;
3031
+ /**
3032
+ * Audio placeholder token id (258881). Each `<audio>` placeholder expands to
3033
+ * `boa + audio_token × n_frames + eoa`.
3034
+ */
3035
+ audioTokenId?: number;
3036
+ /** Begin-of-audio token id (256000), emitted before the audio token run. */
3037
+ boaTokenId?: number;
3038
+ /**
3039
+ * End-of-audio token id, parsed from the config's `eoa_token_index` (258883).
3040
+ * A real appended token (like `eoi`), despite the "index" name.
3041
+ */
3042
+ eoaTokenId?: number;
3043
+ /**
3044
+ * Raw audio samples per audio token (640 = 40 ms @ 16 kHz), from
3045
+ * `audio_config.audio_samples_per_token`. Frame size for the encoder-free
3046
+ * pad+reshape feature extractor.
3047
+ */
3048
+ audioSamplesPerToken?: number;
3049
+ /**
3050
+ * GPU memory budget for paged KV cache in megabytes.
3051
+ * Only used when `use_block_paged_cache` is true.
3052
+ * Default: auto-sized to cover `max_position_embeddings` for the
3053
+ * physical full-attention layers.
3054
+ */
3055
+ pagedCacheMemoryMb?: number | undefined;
3056
+ /**
3057
+ * Block size for paged attention (tokens per block).
3058
+ * Only used when `use_block_paged_cache` is true.
3059
+ * Default: 16.
3060
+ */
3061
+ pagedBlockSize?: number | undefined;
3062
+ /**
3063
+ * Use the new block-paged KV cache adapter (`PagedKVCacheAdapter`).
3064
+ *
3065
+ * When `Some(true)` or unset (the default), `Gemma4Inner` builds
3066
+ * model-independent KV-cache specs, groups them, and allocates a
3067
+ * `BlockAllocator` + `LayerKVPool` pair for physical full-attention
3068
+ * layers. Sliding-window layers still use `RotatingKVCache` until
3069
+ * true paged sliding-window groups are wired. KV-shared layers are
3070
+ * aliases: they reuse their anchor's cache slot and do not allocate
3071
+ * separate physical storage.
3072
+ *
3073
+ * Default: `true` (paged adapter on; opt-out via
3074
+ * `use_block_paged_cache: false` in `config.json` to use the flat
3075
+ * (non-paged) all-`Gemma4LayerCache` path instead). Parity between
3076
+ * the two paths is verified by
3077
+ * `crates/mlx-core/tests/gemma4_paged_vs_flat_parity.rs` against
3078
+ * real Gemma-4-E2B weights.
3079
+ */
3080
+ useBlockPagedCache?: boolean | undefined;
3081
+ }
3082
+
3083
+ /** Optional load-time settings for [`Gemma4Model::load`]. */
3084
+ export interface Gemma4LoadOptions {
3085
+ /**
3086
+ * Directory of a draft checkpoint (config.json + safetensors) to load
3087
+ * alongside the target model for speculative decoding — either a
3088
+ * DSpark draft or a Google assistant draft; the kind is probed from
3089
+ * the draft config.json. When omitted, `<model_path>/draft/` is loaded
3090
+ * automatically when present. Draft decoding runs only on the flat
3091
+ * KV-cache path: setting this while the model config explicitly enables
3092
+ * `use_block_paged_cache` is a hard load error, and an unset
3093
+ * `use_block_paged_cache` is forced to `false`.
3094
+ */
3095
+ draftModelPath?: string;
2868
3096
  }
2869
3097
 
2870
3098
  /**
@@ -2937,19 +3165,19 @@ export interface GenerationConfig {
2937
3165
  /** Number of recent tokens to consider for frequency penalty (default: 20) */
2938
3166
  frequencyContextSize?: number;
2939
3167
  /**
2940
- * Stop if same token repeats this many times consecutively (default: 16)
2941
- * Set to 0 to disable. Prevents OOM from degenerate repetitive generation.
3168
+ * Stop if same token repeats this many times consecutively (default: 0 = disabled).
3169
+ * Opt in by setting a positive value to guard against degenerate repetitive generation.
2942
3170
  */
2943
3171
  maxConsecutiveTokens?: number;
2944
3172
  /**
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".
3173
+ * Stop if a pattern repeats this many times consecutively (default: 0 = disabled).
3174
+ * Opt in with a positive value to detect patterns like "A B A B A B".
2947
3175
  * Uses range-based detection: checks all pattern sizes from 2 to ngram_size.
2948
3176
  */
2949
3177
  maxNgramRepeats?: number;
2950
3178
  /**
2951
- * Maximum pattern size for repetition detection (default: 64)
2952
- * All pattern sizes from 2 up to this value are checked each decode step.
3179
+ * Maximum pattern size for repetition detection (default: 0 = disabled).
3180
+ * When enabled, all pattern sizes from 2 up to this value are checked each decode step.
2953
3181
  * Larger values catch long phrase-level repetition common in small models.
2954
3182
  */
2955
3183
  ngramSize?: number;
@@ -2964,29 +3192,6 @@ export interface GenerationConfig {
2964
3192
  * Set to 0 to disable chunking and process the entire prompt at once.
2965
3193
  */
2966
3194
  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
3195
  }
2991
3196
 
2992
3197
  export interface GenerationProfile {
@@ -3010,6 +3215,24 @@ export interface GenerationProfile {
3010
3215
  timeToFirstTokenMs: number;
3011
3216
  /** Per-phase breakdown. */
3012
3217
  phases: Array<PhaseProfile>;
3218
+ /**
3219
+ * MTP speculative decode: mean accepted *draft* tokens per cycle
3220
+ * (excludes the always-verified token). Historical drafts-only metric.
3221
+ */
3222
+ mtpMeanAcceptedTokens?: number;
3223
+ /**
3224
+ * MTP speculative decode: mean *committed* tokens per cycle, INCLUDING
3225
+ * the always-verified token (`mtp_accepted_drafts_total / mtp_cycles
3226
+ * + 1.0`). mlx-vlm-comparable headline; equals mlx-vlm's
3227
+ * `(accepted_drafts + rounds) / rounds` (`common.py:247`).
3228
+ */
3229
+ mtpMeanAcceptedTokensTotal?: number;
3230
+ /** MTP speculative decode: per-draft-position acceptance rate. */
3231
+ mtpAcceptanceByPosition?: Array<number>;
3232
+ /** MTP speculative decode: number of draft+verify cycles executed. */
3233
+ mtpCycles?: number;
3234
+ /** MTP speculative decode: mean attempted draft depth per cycle. */
3235
+ mtpMeanDepth?: number;
3013
3236
  /** Memory snapshot before generation. */
3014
3237
  memoryBefore?: MemorySnapshot;
3015
3238
  /** Memory snapshot after generation. */
@@ -3036,8 +3259,8 @@ export interface GenerationWithToolCalls {
3036
3259
  toolCalls: Array<ToolCallRecord>;
3037
3260
  }
3038
3261
 
3039
- /** Get expected weight keys for PaddleOCR-VL model */
3040
- export declare function getExpectedWeightKeys(): Array<string>;
3262
+ /** Sample MLX's GPU memory counters. See [`GpuMemorySnapshot`]. */
3263
+ export declare function getMemorySnapshot(): GpuMemorySnapshot;
3041
3264
 
3042
3265
  /** Retrieve all collected profiling data as a `ProfilingSession`. */
3043
3266
  export declare function getProfilingData(): ProfilingSession;
@@ -3047,6 +3270,13 @@ export interface GgufConversionOptions {
3047
3270
  inputPath: string;
3048
3271
  /** Output directory for converted SafeTensors model */
3049
3272
  outputDir: string;
3273
+ /**
3274
+ * Optional directory containing the authoritative HuggingFace config and
3275
+ * tokenizer/processor assets. GGUF metadata is not rich enough to recreate
3276
+ * unified Gemma4 config fields such as head_dim, layer_types, vision, and
3277
+ * audio configuration exactly.
3278
+ */
3279
+ configSourceDir?: string;
3050
3280
  /** Target dtype: "float32", "float16", "bfloat16" (default: keep original) */
3051
3281
  dtype?: string;
3052
3282
  /** Enable verbose logging */
@@ -3080,6 +3310,17 @@ export interface GgufConversionOptions {
3080
3310
  * This makes the safetensors compatible with mlx-vlm.
3081
3311
  */
3082
3312
  vlmKeyPrefix?: boolean;
3313
+ /**
3314
+ * Upgrade quantization to micro-scaling FP (mxfp4 / mxfp8).
3315
+ * When true, applies after the recipe predicate: eligible 8-bit affine
3316
+ * decisions become mxfp8 and 4-bit become mxfp4. Kept affine (not upgraded):
3317
+ * affine-only loaders (lm_head, embed_tokens, router.proj,
3318
+ * embedding_projection) at their recipe bits, MoE router gates (8-bit affine),
3319
+ * and the recipe-pinned attention/GDN projections (o_proj / out_proj /
3320
+ * in_proj_a / in_proj_b, 8-bit affine). Requires `quant_mode = "affine"`.
3321
+ * Forces `group_size = 32` for upgraded layers.
3322
+ */
3323
+ quantMxfp?: boolean;
3083
3324
  }
3084
3325
 
3085
3326
  export interface GgufConversionResult {
@@ -3095,6 +3336,33 @@ export interface GpuInfo {
3095
3336
  architectureGen: number;
3096
3337
  }
3097
3338
 
3339
+ /**
3340
+ * Snapshot of MLX's GPU memory counters at this instant. All values
3341
+ * are in bytes. On Apple Silicon, GPU and CPU share unified memory,
3342
+ * so these are NOT a separate "VRAM" pool — they reflect MLX's own
3343
+ * tracking of `StorageModePrivate` Metal buffers (model weights,
3344
+ * `LayerKVPool`, transient intermediate tensors) attributed to the
3345
+ * MLX runtime in this process.
3346
+ *
3347
+ * Useful for live observability during long-running sessions:
3348
+ *
3349
+ * ```js
3350
+ * const { getMemorySnapshot } = require('@mlx-node/core');
3351
+ * setInterval(() => {
3352
+ * const m = getMemorySnapshot();
3353
+ * console.log(`active=${(m.activeBytes/1e9).toFixed(2)}GB peak=${(m.peakBytes/1e9).toFixed(2)}GB cache=${(m.cacheBytes/1e9).toFixed(2)}GB`);
3354
+ * }, 1000);
3355
+ * ```
3356
+ */
3357
+ export interface GpuMemorySnapshot {
3358
+ /** Current actively-used GPU buffer bytes (excludes cache pool). */
3359
+ activeBytes: number;
3360
+ /** Peak GPU buffer bytes since the last `resetPeakMemory()` call. */
3361
+ peakBytes: number;
3362
+ /** Bytes held in MLX's caching allocator (released by `clearCache`). */
3363
+ cacheBytes: number;
3364
+ }
3365
+
3098
3366
  /** Configuration for the GRPO training engine */
3099
3367
  export interface GrpoEngineConfig {
3100
3368
  /** Learning rate (default: 1e-6) */
@@ -3356,6 +3624,69 @@ export interface Lfm2Config {
3356
3624
  eosTokenId: number;
3357
3625
  bosTokenId: number;
3358
3626
  padTokenId: number;
3627
+ /**
3628
+ * GPU memory budget for paged KV cache in megabytes.
3629
+ * Only used when `use_block_paged_cache` is true.
3630
+ * Default: 2048 (2GB).
3631
+ */
3632
+ pagedCacheMemoryMb?: number | undefined;
3633
+ /**
3634
+ * Block size for paged attention (tokens per block).
3635
+ * Only used when `use_block_paged_cache` is true.
3636
+ * Default: 16.
3637
+ */
3638
+ pagedBlockSize?: number | undefined;
3639
+ /**
3640
+ * Use the new block-paged KV cache adapter (`PagedKVCacheAdapter`).
3641
+ *
3642
+ * Default: `true` since 2026-04-28 (parity-verified via
3643
+ * `crates/mlx-core/tests/lfm2_paged_vs_flat_parity.rs` against real
3644
+ * LFM2.5-1.2B weights: byte-equal greedy decode + prefix-reuse
3645
+ * byte-equal at BF16). Wired through
3646
+ * `Lfm2DecoderLayer::forward_paged_or_flat`.
3647
+ *
3648
+ * Per-layer routing: LFM2's hybrid architecture means only
3649
+ * `full_attention` layers go through the paged adapter; conv layers
3650
+ * stay on the existing flat `Lfm2LayerCache::Conv(ArraysCache)`
3651
+ * storage regardless of this flag. The `LayerKVPool` is sized to
3652
+ * the count of `full_attention` layers and indexed by
3653
+ * attention-ordinal (via `config.full_attn_idxs()`), not by absolute
3654
+ * layer index.
3655
+ *
3656
+ * Opt out with `use_block_paged_cache: Some(false)` to revert to the
3657
+ * fully flat `Lfm2LayerCache` path on all layers.
3658
+ */
3659
+ useBlockPagedCache?: boolean | undefined;
3660
+ /**
3661
+ * MLP intermediate size for the DENSE-in-MoE layers (`layer_idx <
3662
+ * num_dense_layers`). Used DIRECTLY (no 2/3 `computed_ff_dim()` shrink).
3663
+ * Only present on MoE checkpoints.
3664
+ */
3665
+ intermediateSize?: number | undefined;
3666
+ /** Per-expert MLP intermediate size for the sparse MoE layers. */
3667
+ moeIntermediateSize?: number | undefined;
3668
+ /** Total number of routed experts. */
3669
+ numExperts?: number | undefined;
3670
+ /** Top-k experts selected per token. */
3671
+ numExpertsPerTok?: number | undefined;
3672
+ /** Number of leading DENSE layers before MoE layers begin. */
3673
+ numDenseLayers?: number | undefined;
3674
+ /**
3675
+ * Renormalize the top-k routing weights to sum to 1 (`/(sum+1e-20)`).
3676
+ *
3677
+ * `Option<bool>` so TS callers may omit it (napi renders bare `bool` as
3678
+ * required). Absent (None) is read as `true` everywhere via
3679
+ * `.unwrap_or(true)`, matching the prior `default = "default_true"`.
3680
+ */
3681
+ normTopkProb?: boolean | undefined;
3682
+ /**
3683
+ * Add the learned per-expert bias to the post-softmax gates BEFORE top-k.
3684
+ *
3685
+ * `Option<bool>` so TS callers may omit it (napi renders bare `bool` as
3686
+ * required). Absent (None) is read as `true` everywhere via
3687
+ * `.unwrap_or(true)`, matching the prior `default = "default_true"`.
3688
+ */
3689
+ useExpertBias?: boolean | undefined;
3359
3690
  }
3360
3691
 
3361
3692
  export interface MemorySnapshot {
@@ -3367,6 +3698,28 @@ export interface MemorySnapshot {
3367
3698
  cacheBytes: number;
3368
3699
  }
3369
3700
 
3701
+ /**
3702
+ * Return a snapshot of the MLX allocator's memory counters. Primarily
3703
+ * useful for dashboards and for debugging the `MLX_CACHE_LIMIT_GB`
3704
+ * override. Read-only — does not mutate allocator state.
3705
+ */
3706
+ export declare function memoryStats(): MemoryStats;
3707
+
3708
+ /**
3709
+ * Snapshot of the MLX Metal allocator's memory state. All values are in
3710
+ * bytes and returned as `f64` to avoid forcing BigInt round-trips in JS.
3711
+ */
3712
+ export interface MemoryStats {
3713
+ /** Actively-used memory (excludes the cached free-pool). */
3714
+ active: number;
3715
+ /** Peak memory usage since load / the last `resetPeakMemory`. */
3716
+ peak: number;
3717
+ /** Cache / free-pool memory currently held by the allocator. */
3718
+ cache: number;
3719
+ /** Metal `max_recommended_working_set_size` snapshot (0 on non-Metal). */
3720
+ wiredLimit: number;
3721
+ }
3722
+
3370
3723
  /** Full model configuration */
3371
3724
  export interface ModelConfig {
3372
3725
  visionConfig: VisionConfig;
@@ -3410,56 +3763,6 @@ export interface OutputStoreConfig {
3410
3763
  localPath: string;
3411
3764
  }
3412
3765
 
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
3766
  /** A text paragraph */
3464
3767
  export interface Paragraph {
3465
3768
  content: string;
@@ -3541,6 +3844,52 @@ export interface PerformanceMetrics {
3541
3844
  * Excludes the first token (counted as prefill).
3542
3845
  */
3543
3846
  decodeTokensPerSecond: number;
3847
+ /**
3848
+ * MTP speculative decode: mean accepted *draft* tokens per cycle
3849
+ * (range `[0, depth]`). EXCLUDES the always-verified token each cycle
3850
+ * commits. `None` on plain autoregressive runs where no MTP cycle
3851
+ * executed. This is the historical drafts-only metric; for the
3852
+ * mlx-vlm-comparable headline see [`Self::mtp_mean_accepted_tokens_total`].
3853
+ */
3854
+ mtpMeanAcceptedTokens?: number;
3855
+ /**
3856
+ * MTP speculative decode: mean *committed* tokens per cycle, INCLUDING
3857
+ * the single always-verified token each cycle emits — i.e.
3858
+ * `mtp_accepted_drafts_total / mtp_cycles + 1.0`. This is the
3859
+ * mlx-vlm-comparable headline accept rate: it equals mlx-vlm's
3860
+ * `mean_accepted_tokens = (accepted_drafts + rounds) / rounds`
3861
+ * (`mlx-vlm/mlx_vlm/speculative/common.py:247`), where our
3862
+ * `mtp_cycles` is the 1:1 analog of mlx-vlm's `rounds` (one
3863
+ * draft+verify iteration; `record_mtp_cycle` is called exactly once
3864
+ * per cycle). The per-cycle `+1.0` matches mlx-vlm's `+rounds`
3865
+ * assumption — every round commits exactly one verified token
3866
+ * (the residual on partial-accept, the bonus on full-accept), even
3867
+ * when the final cycle's tail is EOS/length-truncated downstream
3868
+ * (mlx-vlm makes the same assumption: it appends to `accept_lens`
3869
+ * once per round regardless of truncation). `None` on plain
3870
+ * autoregressive runs.
3871
+ */
3872
+ mtpMeanAcceptedTokensTotal?: number;
3873
+ /**
3874
+ * MTP speculative decode: per-draft-position acceptance rate
3875
+ * (index = draft position). `None` on plain autoregressive runs.
3876
+ */
3877
+ mtpAcceptanceByPosition?: Array<number>;
3878
+ /**
3879
+ * MTP speculative decode: number of draft+verify cycles executed.
3880
+ * `None` on plain autoregressive runs.
3881
+ */
3882
+ mtpCycles?: number;
3883
+ /**
3884
+ * MTP speculative decode: mean attempted draft depth per cycle.
3885
+ * `None` on plain autoregressive runs.
3886
+ */
3887
+ mtpMeanDepth?: number;
3888
+ /**
3889
+ * Optional decode phase breakdown. Present when decode profiling
3890
+ * is enabled via `MLX_PROFILE_DECODE=1` or `setProfilingEnabled(true)`.
3891
+ */
3892
+ profilePhases?: Array<PhaseProfile>;
3544
3893
  }
3545
3894
 
3546
3895
  export interface PhaseProfile {
@@ -3554,6 +3903,76 @@ export interface PhaseProfile {
3554
3903
  count: number;
3555
3904
  }
3556
3905
 
3906
+ /**
3907
+ * Per-call Viterbi calibration overrides.
3908
+ *
3909
+ * Any field set to `Some(_)` overrides the corresponding bias from the
3910
+ * model's default calibration (loaded from `viterbi_calibration.json`
3911
+ * at load time). Missing fields fall back to the default.
3912
+ */
3913
+ export interface PrivacyCalibration {
3914
+ transitionBiasBackgroundStay?: number;
3915
+ transitionBiasBackgroundToStart?: number;
3916
+ transitionBiasEndToBackground?: number;
3917
+ transitionBiasEndToStart?: number;
3918
+ transitionBiasInsideToContinue?: number;
3919
+ transitionBiasInsideToEnd?: number;
3920
+ }
3921
+
3922
+ /**
3923
+ * Options for [`PrivacyFilterModelJs::classify`].
3924
+ *
3925
+ * - `threshold` (default `0.5`): minimum mean per-token probability for
3926
+ * an extracted span to be returned.
3927
+ * - `calibration`: per-call overrides on top of the checkpoint default.
3928
+ * - `return_tokens` (default `false`): when `true`, the result includes
3929
+ * a `tokens` array with one entry per input token.
3930
+ */
3931
+ export interface PrivacyClassifyOptions {
3932
+ threshold?: number;
3933
+ calibration?: PrivacyCalibration;
3934
+ returnTokens?: boolean;
3935
+ }
3936
+
3937
+ /** Result of [`PrivacyFilterModelJs::classify`]. */
3938
+ export interface PrivacyClassifyResult {
3939
+ entities: Array<PrivacyEntity>;
3940
+ tokens?: Array<PrivacyToken>;
3941
+ }
3942
+
3943
+ /**
3944
+ * A privacy entity detected by [`PrivacyFilterModelJs::classify`].
3945
+ *
3946
+ * `start`/`end` are byte offsets into the input string (Hugging Face
3947
+ * `tokenizers` convention). `label` is the privacy class without the
3948
+ * BIOES prefix (e.g. `"private_email"`). `score` is the mean — across
3949
+ * the span's tokens — of the softmax probability of the Viterbi-emitted
3950
+ * tag at each token.
3951
+ */
3952
+ export interface PrivacyEntity {
3953
+ label: string;
3954
+ start: number;
3955
+ end: number;
3956
+ score: number;
3957
+ text: string;
3958
+ }
3959
+
3960
+ /**
3961
+ * Per-token output emitted when [`PrivacyClassifyOptions::return_tokens`]
3962
+ * is `true`. `tag` is the full BIOES tag (`"O"` or `"B-..."`/`"I-..."`/
3963
+ * `"E-..."`/`"S-..."`) chosen by the Viterbi decoder. `score` is the
3964
+ * softmax probability of that emitted tag at this token, so `tag` and
3965
+ * `score` always share decoders (at boundary tokens the Viterbi tag can
3966
+ * differ from the local argmax).
3967
+ */
3968
+ export interface PrivacyToken {
3969
+ text: string;
3970
+ tag: string;
3971
+ score: number;
3972
+ start: number;
3973
+ end: number;
3974
+ }
3975
+
3557
3976
  export interface ProfilingSession {
3558
3977
  /** GPU hardware info. */
3559
3978
  gpuInfo: GpuInfo;
@@ -3601,6 +4020,32 @@ export interface QianfanOcrConfig {
3601
4020
  minDynamicPatch: number;
3602
4021
  }
3603
4022
 
4023
+ /** Microbench result for the production quantized qmv dispatch path. */
4024
+ export interface QmvQuantizedMicrobenchResult {
4025
+ /** Median `quantized_matmul` wall-clock per call, in nanoseconds. */
4026
+ medianNs: number;
4027
+ /** A tiny materialized checksum of the final output, used to keep the call live. */
4028
+ checksum: number;
4029
+ }
4030
+
4031
+ /**
4032
+ * Run the production quantized-qmv microbench in the current process.
4033
+ *
4034
+ * To compare `MLX_MTP_SMALL_M_QMV=0` versus `1`, call this from separate
4035
+ * processes. MLX caches the env-backed dispatch predicate statically.
4036
+ */
4037
+ export declare function quantizedQmvMicrobench(
4038
+ k: number,
4039
+ n: number,
4040
+ m: number,
4041
+ groupSize: number,
4042
+ bits: number,
4043
+ mode: string,
4044
+ dtype: DType,
4045
+ warmup?: number | undefined | null,
4046
+ iters?: number | undefined | null,
4047
+ ): QmvQuantizedMicrobenchResult;
4048
+
3604
4049
  /**
3605
4050
  * Qwen3.5 model configuration (dense variant).
3606
4051
  *
@@ -3629,6 +4074,72 @@ export interface Qwen35Config {
3629
4074
  fullAttentionInterval: number;
3630
4075
  partialRotaryFactor: number;
3631
4076
  ropeTheta: number;
4077
+ /**
4078
+ * GPU memory budget for paged KV cache in megabytes.
4079
+ * Only used when `use_block_paged_cache` is true.
4080
+ * Default: automatically sized for one full-context sequence.
4081
+ */
4082
+ pagedCacheMemoryMb?: number | undefined;
4083
+ /**
4084
+ * Block size for paged attention (tokens per block).
4085
+ * Only used when `use_block_paged_cache` is true.
4086
+ * Default: 16.
4087
+ */
4088
+ pagedBlockSize?: number | undefined;
4089
+ /**
4090
+ * Use the block-paged KV cache adapter (`PagedKVCacheAdapter`) for
4091
+ * full-attention layers.
4092
+ *
4093
+ * **OPT-IN — experimental.** When `Some(true)`, `Qwen35Inner`
4094
+ * allocates a `BlockAllocator` + `LayerKVPool` pair sized for the
4095
+ * model's full-attention layer count and constructs a
4096
+ * `PagedKVCacheAdapter`. The chat-session forward dispatch routes
4097
+ * full-attention layers through this adapter while linear-attention
4098
+ * (GatedDeltaNet / GDN) layers continue to use the existing
4099
+ * `Qwen3_5LayerCache::Linear(ArraysCache)` path with no
4100
+ * cross-request prefix reuse — vLLM's `MambaManager`-style "no
4101
+ * prefix reuse for recurrent layers" stance.
4102
+ *
4103
+ * **Paged vs flat eager**: this flag selects the eager paged decode
4104
+ * over the eager flat decode. When `Some(true)`, full-attention
4105
+ * layers run through the paged adapter (cross-request prefix reuse);
4106
+ * when unset, they run the eager flat decode. Either way the forward
4107
+ * is pure-Rust eager.
4108
+ *
4109
+ * **VLM under paged**: a VLM checkpoint defaults this flag ON at load, so
4110
+ * dense image turns ONLY run on the paged-vision core. A fresh single-turn
4111
+ * image-bearing prompt prefills through the paged adapter (M-RoPE positions
4112
+ * feed the rotary; the merged vision embeddings feed the forward) and
4113
+ * decodes plain AR — MTP weights are ignored on image turns. Warm
4114
+ * image-bearing session continues / cache-hit reuse are still rejected at
4115
+ * runtime (the GDN two-pass warm prefix is not byte-exact). A vision turn
4116
+ * that reaches a None adapter (explicit `Some(false)`, non-Metal build, or
4117
+ * a sym8 checkpoint) errors at dispatch.
4118
+ *
4119
+ * Default: `None` for text-only checkpoints (eager flat decode);
4120
+ * `Some(true)` for VLM checkpoints (block-paged, set in `parse_config`).
4121
+ */
4122
+ useBlockPagedCache?: boolean | undefined;
4123
+ /**
4124
+ * Number of MTP (Multi-Token Prediction) head layers shipped with the
4125
+ * checkpoint. Populated from `mtp_num_hidden_layers` /
4126
+ * `num_nextn_predict_layers` in `config.json`. `0` means the
4127
+ * checkpoint has no MTP heads and the speculative-decode path is
4128
+ * unavailable.
4129
+ */
4130
+ nMtpLayers: number;
4131
+ }
4132
+
4133
+ /**
4134
+ * Trained and physically available active-context limits for one loaded
4135
+ * Qwen3.5 model. Values are snapshots because the physical pool is fixed for
4136
+ * the lifetime of the resident model.
4137
+ */
4138
+ export interface Qwen35ContextLimits {
4139
+ trainedWindowTokens: number;
4140
+ effectiveWindowTokens: number;
4141
+ pagedBlockCapacity: number;
4142
+ pagedBlockSize: number;
3632
4143
  }
3633
4144
 
3634
4145
  /** Generation configuration for Qwen3.5 */
@@ -3683,6 +4194,47 @@ export interface Qwen35MoeConfig {
3683
4194
  moeIntermediateSize?: number | undefined;
3684
4195
  normTopkProb: boolean;
3685
4196
  mlpOnlyLayers?: number[] | undefined;
4197
+ /**
4198
+ * GPU memory budget for paged KV cache in megabytes.
4199
+ * Only used when `use_block_paged_cache` is true.
4200
+ * Default: automatically sized for one full-context sequence.
4201
+ */
4202
+ pagedCacheMemoryMb?: number | undefined;
4203
+ /**
4204
+ * Block size for paged attention (tokens per block).
4205
+ * Only used when `use_block_paged_cache` is true.
4206
+ * Default: 16.
4207
+ */
4208
+ pagedBlockSize?: number | undefined;
4209
+ /**
4210
+ * Use the block-paged KV cache adapter for full-attention layers.
4211
+ *
4212
+ * **OPT-IN — experimental.** Same semantics as the dense
4213
+ * `Qwen3_5Config::use_block_paged_cache` field. Selects the eager
4214
+ * paged decode over the eager flat decode: routes full-attention
4215
+ * layers through `PagedKVCacheAdapter` (cross-request prefix reuse);
4216
+ * GDN linear-attention layers stay on `Qwen3_5LayerCache::Linear`
4217
+ * either way. When disabled, full-attention layers run the eager flat
4218
+ * decode instead.
4219
+ *
4220
+ * **VLM under paged**: a VLM checkpoint loads with this flag set, and a
4221
+ * fresh single-turn image-bearing prompt prefills through the paged
4222
+ * adapter (M-RoPE positions feed the rotary; the merged vision embeddings
4223
+ * feed the forward). Image-bearing MTP turns are still rejected at
4224
+ * runtime; warm image-bearing session continues / cache-hit reuse are
4225
+ * cold-started (no warm GDN two-pass prefix).
4226
+ *
4227
+ * Default: `None` / `false`.
4228
+ */
4229
+ useBlockPagedCache?: boolean | undefined;
4230
+ /**
4231
+ * Number of MTP (Multi-Token Prediction) head layers shipped with
4232
+ * the checkpoint. Populated from `mtp_num_hidden_layers` /
4233
+ * `num_nextn_predict_layers` in `config.json`. `0` means the
4234
+ * checkpoint has no MTP heads and the speculative-decode path is
4235
+ * unavailable.
4236
+ */
4237
+ nMtpLayers: number;
3686
4238
  }
3687
4239
 
3688
4240
  /** Generation configuration for Qwen3.5 MoE */
@@ -3719,29 +4271,29 @@ export interface Qwen3Config {
3719
4271
  padTokenId: number;
3720
4272
  eosTokenId: number;
3721
4273
  bosTokenId: number;
3722
- /**
3723
- * Enable paged attention for memory-efficient inference.
3724
- * Default: false (use standard KVCache)
3725
- */
3726
- usePagedAttention?: boolean | undefined;
3727
4274
  /**
3728
4275
  * GPU memory budget for paged KV cache in megabytes.
3729
- * Only used when use_paged_attention is true.
3730
4276
  * Default: 2048 (2GB)
3731
4277
  */
3732
4278
  pagedCacheMemoryMb?: number | undefined;
3733
4279
  /**
3734
4280
  * Block size for paged attention (tokens per block).
3735
- * Only used when use_paged_attention is true.
3736
4281
  * Default: 16
3737
4282
  */
3738
4283
  pagedBlockSize?: number | undefined;
3739
4284
  /**
3740
- * Use FP8 cache for 2x memory reduction (experimental).
3741
- * Only used when use_paged_attention is true.
3742
- * Default: false
4285
+ * Use the block-paged KV cache adapter (`PagedKVCacheAdapter`).
4286
+ *
4287
+ * When `Some(true)` (the default for Qwen3), `Qwen3Inner` allocates a
4288
+ * `BlockAllocator` + `LayerKVPool` pair and constructs a
4289
+ * `PagedKVCacheAdapter` for cross-request KV prefix reuse (vLLM-style
4290
+ * block-paged storage with refcounted prefix caching). When
4291
+ * `Some(false)`, the flat (non-paged) `Vec<KVCache>` cache path is
4292
+ * used instead.
4293
+ *
4294
+ * Default: true.
3743
4295
  */
3744
- useFp8Cache?: boolean | undefined;
4296
+ useBlockPagedCache?: boolean | undefined;
3745
4297
  }
3746
4298
 
3747
4299
  /** Qwen3 language model configuration */
@@ -3768,6 +4320,14 @@ export interface RecResult {
3768
4320
  score: number;
3769
4321
  }
3770
4322
 
4323
+ /**
4324
+ * Reset MLX's peak-memory counter to the current active level.
4325
+ * Useful for measuring per-request peak memory in a long-running
4326
+ * process — call before a request, sample
4327
+ * `getMemorySnapshot().peakBytes` after.
4328
+ */
4329
+ export declare function resetPeakMemory(): void;
4330
+
3771
4331
  /** Clear all collected profiling data and reset session timer. */
3772
4332
  export declare function resetProfilingData(): void;
3773
4333
 
@@ -3857,22 +4417,6 @@ export interface SamplingConfig {
3857
4417
  */
3858
4418
  export declare function saveToXlsx(text: string, filePath: string): void;
3859
4419
 
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
4420
  /** Enable or disable profiling globally. */
3877
4421
  export declare function setProfilingEnabled(enabled: boolean): void;
3878
4422
 
@@ -4155,6 +4699,35 @@ export interface TrainStepResultWithOutputs {
4155
4699
  completionLengths: Array<number>;
4156
4700
  }
4157
4701
 
4702
+ /**
4703
+ * Encoder-free vision configuration for the Gemma 4 unified multimodal model.
4704
+ *
4705
+ * Parsed from the `vision_config` sub-dict of a `gemma4_unified` checkpoint
4706
+ * (`model_type == "gemma4_unified_vision"`). This is a different shape from the
4707
+ * SigLIP-style [`super::vision_config::Gemma4VisionConfig`] used by the dense
4708
+ * gemma4 family: the unified vision path has no transformer encoder, only a
4709
+ * patch embedder (LayerNorm + Linear + 2D positional embedding) feeding the
4710
+ * multimodal projection.
4711
+ */
4712
+ export interface UnifiedVisionConfig {
4713
+ /** Pixel side length of a single image patch (48 = patch_size 16 × pooling 3). */
4714
+ modelPatchSize: number;
4715
+ /** Embedding width inside the vision embedder (3840, == text hidden_size). */
4716
+ mmEmbedDim: number;
4717
+ /** Number of rows in the 2D positional-embedding table (1120). */
4718
+ mmPosembSize: number;
4719
+ /** Maximum soft tokens (patches) per image after resize (280). */
4720
+ numSoftTokens: number;
4721
+ /** Output projection width of `embed_vision` (3840, == text hidden_size). */
4722
+ outputProjDims: number;
4723
+ /** Pixel-grid patch size used by the resize math (16). */
4724
+ patchSize: number;
4725
+ /** Pooling kernel size used by the resize math (3). */
4726
+ poolingKernelSize: number;
4727
+ /** Epsilon for the embedder LayerNorms and the projection RMSNorm. */
4728
+ rmsNormEps: number;
4729
+ }
4730
+
4158
4731
  /** Result from document unwarping. */
4159
4732
  export interface UnwarpResult {
4160
4733
  /** Unwarped image as PNG bytes */
@@ -4224,3 +4797,27 @@ export interface VlmChatMessage {
4224
4797
  /** Text content of the message */
4225
4798
  content: string;
4226
4799
  }
4800
+
4801
+ export declare namespace __internal__ {
4802
+ /**
4803
+ * Drain the MLX allocator's free-pool.
4804
+ *
4805
+ * @internal
4806
+ *
4807
+ * This is a process-wide drain routed through MLX's default-stream
4808
+ * `mlx_synchronize()`, which does NOT wait on the custom generation
4809
+ * streams that the per-model threads run on. Calling this from user
4810
+ * code while a decode is in flight can race live Metal command buffers
4811
+ * and risk use-after-free. The only safe caller today is
4812
+ * `@mlx-node/server`'s idle sweeper, which only triggers after the
4813
+ * in-flight request counter has returned to zero.
4814
+ *
4815
+ * Exposed under the `__internal__` NAPI namespace — reachable as
4816
+ * `require('@mlx-node/core').__internal__.clearCache()` and NOT on
4817
+ * the root `require('@mlx-node/core')` object. The namespace prefix
4818
+ * is a deliberate speed-bump that forces any caller to acknowledge
4819
+ * this is a private drain with custom-stream caveats; the root
4820
+ * surface stays clean of the footgun.
4821
+ */
4822
+ export function clearCache(): void;
4823
+ }