@mlx-node/server 0.0.12 → 0.0.15

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 (45) hide show
  1. package/dist/host/discover.d.ts +3 -6
  2. package/dist/host/discover.d.ts.map +1 -1
  3. package/dist/host/discover.js +9 -42
  4. package/dist/host/index.d.ts +2 -2
  5. package/dist/host/index.d.ts.map +1 -1
  6. package/dist/host/index.js +8 -1
  7. package/package.json +9 -4
  8. package/src/auth.ts +111 -0
  9. package/src/chat-session-warm-reuse.ts +96 -0
  10. package/src/endpoints/messages-count-tokens.ts +164 -0
  11. package/src/endpoints/messages.ts +1802 -0
  12. package/src/endpoints/models.ts +20 -0
  13. package/src/endpoints/responses.ts +3928 -0
  14. package/src/errors.ts +120 -0
  15. package/src/handler.ts +195 -0
  16. package/src/health.ts +213 -0
  17. package/src/host/discover.ts +25 -0
  18. package/src/host/env-policy.ts +81 -0
  19. package/src/host/index.ts +496 -0
  20. package/src/host/logger.ts +419 -0
  21. package/src/host/net.ts +100 -0
  22. package/src/host/paths.ts +77 -0
  23. package/src/host/swap.ts +200 -0
  24. package/src/host/temp-root.ts +110 -0
  25. package/src/idle-sweeper.ts +555 -0
  26. package/src/index.ts +114 -0
  27. package/src/load-model.ts +92 -0
  28. package/src/mappers/anthropic-request.ts +485 -0
  29. package/src/mappers/anthropic-response.ts +306 -0
  30. package/src/mappers/request.ts +456 -0
  31. package/src/mappers/response.ts +163 -0
  32. package/src/model-work-coordinator.ts +416 -0
  33. package/src/pending-writes.ts +481 -0
  34. package/src/registry.ts +691 -0
  35. package/src/router.ts +220 -0
  36. package/src/server.ts +579 -0
  37. package/src/session-registry.ts +1371 -0
  38. package/src/stop-sequence-buffer.ts +161 -0
  39. package/src/streaming.ts +205 -0
  40. package/src/text-recovery.ts +41 -0
  41. package/src/timing.ts +236 -0
  42. package/src/tool-call-buffer.ts +78 -0
  43. package/src/transport-visibility.ts +185 -0
  44. package/src/types-anthropic.ts +409 -0
  45. package/src/types.ts +470 -0
package/src/types.ts ADDED
@@ -0,0 +1,470 @@
1
+ /** OpenAI Responses API types: request/response shapes and SSE streaming events for POST /v1/responses. */
2
+
3
+ export interface InputTextPart {
4
+ type: 'input_text';
5
+ text: string;
6
+ }
7
+
8
+ /**
9
+ * An image attached to a user message. `image_url` may be an `http(s)://` URL
10
+ * (not fetched by the mapper) or a `data:<mime>;base64,<payload>` URL
11
+ * (decoded and forwarded to the model as raw bytes).
12
+ */
13
+ export interface InputImagePart {
14
+ type: 'input_image';
15
+ image_url?: string;
16
+ file_id?: string;
17
+ detail?: 'auto' | 'low' | 'high';
18
+ }
19
+
20
+ /**
21
+ * Echoed back by clients (Codex, pi-ai, etc.) when they replay prior
22
+ * assistant turns in `input[]` instead of using `previous_response_id`.
23
+ * Equivalent to `InputTextPart` for mapping purposes — we only need the text.
24
+ */
25
+ export interface InputAssistantTextPart {
26
+ type: 'output_text';
27
+ text: string;
28
+ annotations?: never[];
29
+ }
30
+
31
+ /** Echoed back by clients when replaying a refusal block from a prior turn. */
32
+ export interface InputRefusalPart {
33
+ type: 'refusal';
34
+ refusal: string;
35
+ }
36
+
37
+ /**
38
+ * Some clients inline a reasoning summary as a content part on an assistant
39
+ * message instead of as a top-level `reasoning` input item.
40
+ */
41
+ export interface InputSummaryTextPart {
42
+ type: 'summary_text';
43
+ text: string;
44
+ }
45
+
46
+ export type ContentPart =
47
+ | InputTextPart
48
+ | InputImagePart
49
+ | InputAssistantTextPart
50
+ | InputRefusalPart
51
+ | InputSummaryTextPart;
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Input items
55
+ // ---------------------------------------------------------------------------
56
+
57
+ export interface InputMessage {
58
+ type?: 'message';
59
+ role: 'user' | 'assistant' | 'system' | 'developer';
60
+ content: string | ContentPart[];
61
+ }
62
+
63
+ export interface InputFunctionCall {
64
+ type: 'function_call';
65
+ id: string;
66
+ call_id: string;
67
+ name: string;
68
+ arguments: string;
69
+ }
70
+
71
+ export interface InputFunctionCallOutput {
72
+ type: 'function_call_output';
73
+ call_id: string;
74
+ output: string;
75
+ }
76
+
77
+ /**
78
+ * Top-level reasoning item replayed by clients that echo prior assistant
79
+ * reasoning summaries instead of using `previous_response_id`. The summary
80
+ * is coalesced onto the next assistant `ChatMessage` as `reasoningContent`.
81
+ */
82
+ export interface InputReasoningItem {
83
+ type: 'reasoning';
84
+ id?: string;
85
+ summary?: { type?: 'summary_text'; text: string }[];
86
+ encrypted_content?: string;
87
+ }
88
+
89
+ export type InputItem = InputMessage | InputFunctionCall | InputFunctionCallOutput | InputReasoningItem;
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Tool definitions (Responses API shape)
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export interface ResponsesToolDefinition {
96
+ type: 'function';
97
+ name: string;
98
+ description?: string;
99
+ parameters?: Record<string, unknown>;
100
+ strict?: boolean;
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Request
105
+ // ---------------------------------------------------------------------------
106
+
107
+ export interface ResponsesAPIRequest {
108
+ model: string;
109
+ input: string | InputItem[];
110
+ instructions?: string;
111
+ tools?: ResponsesToolDefinition[];
112
+ tool_choice?: 'auto' | 'required' | 'none' | { type: 'function'; name: string };
113
+ stream?: boolean;
114
+ temperature?: number;
115
+ top_p?: number;
116
+ max_output_tokens?: number;
117
+ reasoning?: { effort?: string; summary?: string };
118
+ previous_response_id?: string;
119
+ store?: boolean;
120
+ /**
121
+ * Security domain for native content-addressed prefix-cache reuse.
122
+ * Identical prompts only share physical KV blocks when this non-empty value
123
+ * is also identical. Values are limited to 256 UTF-8 bytes. Multi-tenant
124
+ * callers should derive a high-entropy stable value
125
+ * from the authenticated tenant identity. This is independent from
126
+ * `prompt_cache_key`, which controls warm conversation affinity.
127
+ */
128
+ cache_salt?: string;
129
+ /**
130
+ * Stable caller-supplied key identifying the logical conversation for
131
+ * warm-session reuse across stateless turns. Stateless agent clients
132
+ * (pi-mono, Aider, Codex CLI, etc.) own the conversation history
133
+ * client-side and resend the full transcript on every turn — they do
134
+ * NOT use `previous_response_id` — so the server-side
135
+ * `SessionRegistry` can only reuse a warm `ChatSession` across those
136
+ * turns if the client threads a stable key through every request.
137
+ *
138
+ * When present (and `previous_response_id` is absent), the registry's
139
+ * tier-2 lookup scans for a still-live entry whose `promptCacheKey`
140
+ * matches and whose `instructions` are byte-equal to this request's
141
+ * — on a match it leases the warm session out, so the native prefix-
142
+ * cache verifier (`verify_cache_prefix_direct`) can skip the re-
143
+ * prefill of the conversation history and only prefill the newly
144
+ * appended user turn. When absent, each stateless turn cold-starts.
145
+ *
146
+ * `previous_response_id` unconditionally wins when both are set —
147
+ * the two keys may identify different conversation branches, so
148
+ * picking the prompt_cache_key branch on a prev-id miss would risk
149
+ * routing the request through the wrong warm state. Fall through to
150
+ * fresh on prev-id miss instead.
151
+ *
152
+ * **Enabled by default.** Opt out with
153
+ * `MLX_DISABLE_PROMPT_CACHE_KEY=1` in multi-tenant deployments,
154
+ * where the tier-2 lookup becomes unsafe — the key is caller-
155
+ * controlled, so two clients picking the same raw key would lease
156
+ * each other's warm sessions. HMAC-scoping with a boot-time nonce
157
+ * hides the raw value from memory dumps but does not protect
158
+ * against that shared-key hijack. Operators who need multi-tenant
159
+ * isolation should either disable the feature or front the server
160
+ * with an auth proxy that rewrites `prompt_cache_key` per tenant.
161
+ *
162
+ * **Prerequisites (ALL must hold, else the field is a silent no-op):**
163
+ *
164
+ * 1. `MLX_DISABLE_PROMPT_CACHE_KEY` must NOT be set to `"1"` in the
165
+ * server environment (default behavior is enabled).
166
+ * 2. The key must be at least 8 characters. Shorter values
167
+ * (including the empty string) are silently treated as if no
168
+ * key were supplied — trivial guessing collisions on short
169
+ * keys would be a real risk even in single-tenant use.
170
+ * 3. `previous_response_id` must NOT be set on the same request.
171
+ * Prev-id takes precedence; tier-2 never runs when both are
172
+ * present.
173
+ *
174
+ * When any prerequisite fails the server FALLS BACK silently to a
175
+ * cold-start for this turn — no error, no 4xx. Integrators who
176
+ * depend on warm reuse should verify via the `X-Session-Cache`
177
+ * response header: `prefix_hit` means tier-2 engaged AND the
178
+ * native prefix verifier reused tokens; `fresh` means no reuse.
179
+ */
180
+ prompt_cache_key?: string;
181
+ /**
182
+ * OpenAI-reserved `metadata` slot, repurposed here to carry MLX-Node
183
+ * extensions. Today this only exposes `retention_seconds` as a
184
+ * per-request override of `ServerConfig.responseRetentionSec`;
185
+ * unrelated keys are accepted and ignored (additive, forward-compat).
186
+ */
187
+ metadata?: {
188
+ /**
189
+ * Per-request retention override for the stored response row, in
190
+ * seconds. Must be a finite positive integer in `[60, 90 * 86400]`
191
+ * (1 minute … 90 days). Out-of-range or non-integer values return
192
+ * 400. When omitted, the server-wide default applies.
193
+ */
194
+ retention_seconds?: number;
195
+ };
196
+ /**
197
+ * MLX-Node extension carrier for non-OpenAI fields, namespaced under
198
+ * `extra_body` to mirror the OpenAI SDK convention for vendor
199
+ * passthrough. Unknown keys are ignored (additive, forward-compat).
200
+ *
201
+ * Currently exposes:
202
+ * * `generation_mode`: `"mtp"` forces W6 speculative-decode (sets
203
+ * `enableMtp = true`), `"ar"` forces plain autoregressive
204
+ * (`enableMtp = false`). Absent / null / unrecognized leaves
205
+ * `enableMtp` untouched so the downstream `ChatSession` auto-
206
+ * default (true when the model ships an MTP head) applies.
207
+ * * `mtp_depth`: positive integer override for the per-call draft
208
+ * depth. The server forwards any positive integer ≤ 64 (a sanity
209
+ * ceiling that only blocks garbage) and the native per-family
210
+ * `resolve_params` owns the real clamps: qwen3.5 native MTP
211
+ * [1, 5], gemma4 DSpark capped at the draft block size, gemma4
212
+ * assistant drafts [1, 8].
213
+ */
214
+ extra_body?: {
215
+ // Typed as `string | null` (not the literal union `'mtp' | 'ar'`)
216
+ // because the value arrives off-wire and may carry any client-
217
+ // supplied payload. The mapper validates by exact-string match;
218
+ // anything that doesn't match is silently ignored so the auto-
219
+ // default still applies.
220
+ generation_mode?: string | null;
221
+ mtp_depth?: number | null;
222
+ };
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Output items
227
+ // ---------------------------------------------------------------------------
228
+
229
+ export interface OutputTextPart {
230
+ type: 'output_text';
231
+ text: string;
232
+ annotations: never[];
233
+ }
234
+
235
+ export interface SummaryTextPart {
236
+ type: 'summary_text';
237
+ text: string;
238
+ }
239
+
240
+ export interface MessageOutputItem {
241
+ id: string;
242
+ type: 'message';
243
+ role: 'assistant';
244
+ status: 'completed' | 'incomplete' | 'in_progress';
245
+ content: OutputTextPart[];
246
+ }
247
+
248
+ export interface ReasoningOutputItem {
249
+ id: string;
250
+ type: 'reasoning';
251
+ summary: SummaryTextPart[];
252
+ }
253
+
254
+ export interface FunctionCallOutputItem {
255
+ id: string;
256
+ type: 'function_call';
257
+ call_id: string;
258
+ name: string;
259
+ arguments: string;
260
+ status: 'completed' | 'incomplete';
261
+ }
262
+
263
+ export type OutputItem = MessageOutputItem | ReasoningOutputItem | FunctionCallOutputItem;
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Usage
267
+ // ---------------------------------------------------------------------------
268
+
269
+ export interface ResponseUsage {
270
+ input_tokens: number;
271
+ output_tokens: number;
272
+ output_tokens_details: { reasoning_tokens: number };
273
+ total_tokens: number;
274
+ /**
275
+ * Mirrors the upstream OpenAI Responses API `usage.input_tokens_details`
276
+ * object. Populated only when the native dispatch reports
277
+ * `cachedTokens > 0` — a non-zero value means that many prompt tokens
278
+ * were served from the reused KV-cache prefix on this turn. The
279
+ * `X-Cached-Tokens` response header carries the same number on
280
+ * non-streaming responses, but SSE flushes its headers before the
281
+ * native prefix verifier runs, so streaming clients have to read the
282
+ * value out of the terminal `response.completed` event's
283
+ * `usage.input_tokens_details.cached_tokens` field to verify
284
+ * cache-reuse (see Round 5 Fix #3: streaming `X-Session-Cache` is
285
+ * documented as non-authoritative on the streaming path — this
286
+ * in-band number is the authoritative signal).
287
+ */
288
+ input_tokens_details?: { cached_tokens: number };
289
+ /**
290
+ * Server-extension timing fields surfaced for verbose logs. These
291
+ * are native/server inference measurements, distinct from the HTTP
292
+ * logger's outer `elapsedMs` envelope. Unknown fields are ignored by
293
+ * OpenAI-compatible clients.
294
+ */
295
+ time_to_first_token_ms?: number;
296
+ prefill_tokens_per_second?: number;
297
+ decode_tokens_per_second?: number;
298
+ server_inference_elapsed_ms?: number;
299
+ server_time_to_first_token_ms?: number;
300
+ server_total_time_to_first_token_ms?: number;
301
+ server_prefill_tokens_per_second?: number;
302
+ server_decode_tokens_per_second?: number;
303
+ server_model_resolve_ms?: number;
304
+ server_load_wait_ms?: number;
305
+ server_load_owner?: boolean;
306
+ server_queue_ms?: number;
307
+ server_pre_inference_ms?: number;
308
+ server_paged_prefill_chunk_size?: number;
309
+ server_paged_prefill_eval_interval?: number;
310
+ server_paged_decode_cache_clear_interval?: number;
311
+ /**
312
+ * Server-extension cache context for `prefill_tokens_per_second`.
313
+ * On cached-prefix turns, `prefill_input_tokens` is the uncached
314
+ * suffix that was actually prefetched and `cached_prefix_tokens`
315
+ * is the skipped prefix, so verbose logs do not mistake suffix-only
316
+ * prefill throughput for full-prompt throughput.
317
+ */
318
+ prefill_input_tokens?: number;
319
+ cached_prefix_tokens?: number;
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Error
324
+ // ---------------------------------------------------------------------------
325
+
326
+ export interface ResponseError {
327
+ type: string;
328
+ message: string;
329
+ code: string | null;
330
+ param: string | null;
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Response object
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export interface ResponseObject {
338
+ id: string;
339
+ object: 'response';
340
+ created_at: number;
341
+ status: 'completed' | 'incomplete' | 'in_progress' | 'failed';
342
+ model: string;
343
+ output: OutputItem[];
344
+ output_text: string;
345
+ error: ResponseError | null;
346
+ incomplete_details: { reason: string } | null;
347
+ usage: ResponseUsage;
348
+ instructions: string | null;
349
+ temperature: number | null;
350
+ top_p: number | null;
351
+ max_output_tokens: number | null;
352
+ tools: ResponsesToolDefinition[];
353
+ tool_choice: 'auto' | 'required' | 'none' | { type: 'function'; name: string } | null;
354
+ reasoning: { effort?: string; summary?: string } | null;
355
+ previous_response_id: string | null;
356
+ }
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // Streaming events
360
+ // ---------------------------------------------------------------------------
361
+
362
+ export interface ResponseCreatedEvent {
363
+ type: 'response.created';
364
+ response: ResponseObject;
365
+ }
366
+
367
+ export interface ResponseInProgressEvent {
368
+ type: 'response.in_progress';
369
+ response: ResponseObject;
370
+ }
371
+
372
+ export interface ResponseCompletedEvent {
373
+ type: 'response.completed';
374
+ response: ResponseObject;
375
+ }
376
+
377
+ export interface ResponseFailedEvent {
378
+ type: 'response.failed';
379
+ response: ResponseObject;
380
+ }
381
+
382
+ export interface OutputItemAddedEvent {
383
+ type: 'response.output_item.added';
384
+ output_index: number;
385
+ item: OutputItem;
386
+ }
387
+
388
+ export interface OutputItemDoneEvent {
389
+ type: 'response.output_item.done';
390
+ output_index: number;
391
+ item: OutputItem;
392
+ }
393
+
394
+ export interface ContentPartAddedEvent {
395
+ type: 'response.content_part.added';
396
+ item_id: string;
397
+ output_index: number;
398
+ content_index: number;
399
+ part: OutputTextPart;
400
+ }
401
+
402
+ export interface ContentPartDoneEvent {
403
+ type: 'response.content_part.done';
404
+ item_id: string;
405
+ output_index: number;
406
+ content_index: number;
407
+ part: OutputTextPart;
408
+ }
409
+
410
+ export interface OutputTextDeltaEvent {
411
+ type: 'response.output_text.delta';
412
+ item_id: string;
413
+ output_index: number;
414
+ content_index: number;
415
+ delta: string;
416
+ }
417
+
418
+ export interface OutputTextDoneEvent {
419
+ type: 'response.output_text.done';
420
+ item_id: string;
421
+ output_index: number;
422
+ content_index: number;
423
+ text: string;
424
+ }
425
+
426
+ export interface ReasoningSummaryTextDeltaEvent {
427
+ type: 'response.reasoning_summary_text.delta';
428
+ item_id: string;
429
+ output_index: number;
430
+ summary_index: number;
431
+ delta: string;
432
+ }
433
+
434
+ export interface ReasoningSummaryTextDoneEvent {
435
+ type: 'response.reasoning_summary_text.done';
436
+ item_id: string;
437
+ output_index: number;
438
+ summary_index: number;
439
+ text: string;
440
+ }
441
+
442
+ export interface FunctionCallArgumentsDeltaEvent {
443
+ type: 'response.function_call_arguments.delta';
444
+ item_id: string;
445
+ output_index: number;
446
+ delta: string;
447
+ }
448
+
449
+ export interface FunctionCallArgumentsDoneEvent {
450
+ type: 'response.function_call_arguments.done';
451
+ item_id: string;
452
+ output_index: number;
453
+ arguments: string;
454
+ }
455
+
456
+ export type StreamEvent =
457
+ | ResponseCreatedEvent
458
+ | ResponseInProgressEvent
459
+ | ResponseCompletedEvent
460
+ | ResponseFailedEvent
461
+ | OutputItemAddedEvent
462
+ | OutputItemDoneEvent
463
+ | ContentPartAddedEvent
464
+ | ContentPartDoneEvent
465
+ | OutputTextDeltaEvent
466
+ | OutputTextDoneEvent
467
+ | ReasoningSummaryTextDeltaEvent
468
+ | ReasoningSummaryTextDoneEvent
469
+ | FunctionCallArgumentsDeltaEvent
470
+ | FunctionCallArgumentsDoneEvent;