@mlx-node/server 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/auth.d.ts +56 -0
  2. package/dist/auth.d.ts.map +1 -0
  3. package/dist/auth.js +106 -0
  4. package/dist/chat-session-warm-reuse.d.ts +51 -0
  5. package/dist/chat-session-warm-reuse.d.ts.map +1 -0
  6. package/dist/chat-session-warm-reuse.js +72 -0
  7. package/dist/endpoints/messages-count-tokens.d.ts +8 -0
  8. package/dist/endpoints/messages-count-tokens.d.ts.map +1 -0
  9. package/dist/endpoints/messages-count-tokens.js +121 -0
  10. package/dist/endpoints/messages.d.ts +57 -5
  11. package/dist/endpoints/messages.d.ts.map +1 -1
  12. package/dist/endpoints/messages.js +1043 -147
  13. package/dist/endpoints/models.d.ts +2 -1
  14. package/dist/endpoints/models.d.ts.map +1 -1
  15. package/dist/endpoints/models.js +2 -2
  16. package/dist/endpoints/responses.d.ts +22 -7
  17. package/dist/endpoints/responses.d.ts.map +1 -1
  18. package/dist/endpoints/responses.js +608 -85
  19. package/dist/errors.d.ts +1 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +3 -0
  22. package/dist/handler.d.ts +69 -1
  23. package/dist/handler.d.ts.map +1 -1
  24. package/dist/handler.js +70 -16
  25. package/dist/health.d.ts +146 -0
  26. package/dist/health.d.ts.map +1 -0
  27. package/dist/health.js +107 -0
  28. package/dist/host/discover.d.ts +19 -0
  29. package/dist/host/discover.d.ts.map +1 -0
  30. package/dist/host/discover.js +50 -0
  31. package/dist/host/env-policy.d.ts +62 -0
  32. package/dist/host/env-policy.d.ts.map +1 -0
  33. package/dist/host/env-policy.js +69 -0
  34. package/dist/host/index.d.ts +202 -0
  35. package/dist/host/index.d.ts.map +1 -0
  36. package/dist/host/index.js +325 -0
  37. package/dist/host/logger.d.ts +36 -0
  38. package/dist/host/logger.d.ts.map +1 -0
  39. package/dist/host/logger.js +376 -0
  40. package/dist/host/net.d.ts +65 -0
  41. package/dist/host/net.d.ts.map +1 -0
  42. package/dist/host/net.js +97 -0
  43. package/dist/host/paths.d.ts +28 -0
  44. package/dist/host/paths.d.ts.map +1 -0
  45. package/dist/host/paths.js +71 -0
  46. package/dist/host/swap.d.ts +27 -0
  47. package/dist/host/swap.d.ts.map +1 -0
  48. package/dist/host/swap.js +178 -0
  49. package/dist/host/temp-root.d.ts +57 -0
  50. package/dist/host/temp-root.d.ts.map +1 -0
  51. package/dist/host/temp-root.js +99 -0
  52. package/dist/idle-sweeper.d.ts +245 -0
  53. package/dist/idle-sweeper.d.ts.map +1 -0
  54. package/dist/idle-sweeper.js +408 -0
  55. package/dist/index.d.ts +19 -4
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +17 -1
  58. package/dist/load-model.d.ts +69 -0
  59. package/dist/load-model.d.ts.map +1 -0
  60. package/dist/load-model.js +63 -0
  61. package/dist/mappers/anthropic-request.d.ts +24 -2
  62. package/dist/mappers/anthropic-request.d.ts.map +1 -1
  63. package/dist/mappers/anthropic-request.js +222 -24
  64. package/dist/mappers/anthropic-response.d.ts +29 -4
  65. package/dist/mappers/anthropic-response.d.ts.map +1 -1
  66. package/dist/mappers/anthropic-response.js +143 -21
  67. package/dist/mappers/request.d.ts +48 -0
  68. package/dist/mappers/request.d.ts.map +1 -1
  69. package/dist/mappers/request.js +211 -35
  70. package/dist/mappers/response.d.ts.map +1 -1
  71. package/dist/mappers/response.js +13 -1
  72. package/dist/model-work-coordinator.d.ts +95 -0
  73. package/dist/model-work-coordinator.d.ts.map +1 -0
  74. package/dist/model-work-coordinator.js +201 -0
  75. package/dist/pending-writes.d.ts.map +1 -1
  76. package/dist/presets.d.ts +82 -0
  77. package/dist/presets.d.ts.map +1 -0
  78. package/dist/presets.js +98 -0
  79. package/dist/registry.d.ts +31 -1
  80. package/dist/registry.d.ts.map +1 -1
  81. package/dist/registry.js +33 -5
  82. package/dist/router.d.ts +37 -1
  83. package/dist/router.d.ts.map +1 -1
  84. package/dist/router.js +79 -7
  85. package/dist/server.d.ts +193 -3
  86. package/dist/server.d.ts.map +1 -1
  87. package/dist/server.js +173 -10
  88. package/dist/session-registry.d.ts +279 -18
  89. package/dist/session-registry.d.ts.map +1 -1
  90. package/dist/session-registry.js +518 -37
  91. package/dist/stop-sequence-buffer.d.ts +58 -0
  92. package/dist/stop-sequence-buffer.d.ts.map +1 -0
  93. package/dist/stop-sequence-buffer.js +148 -0
  94. package/dist/streaming.d.ts +14 -0
  95. package/dist/streaming.d.ts.map +1 -1
  96. package/dist/streaming.js +45 -0
  97. package/dist/text-recovery.d.ts +35 -0
  98. package/dist/text-recovery.d.ts.map +1 -0
  99. package/dist/text-recovery.js +41 -0
  100. package/dist/timing.d.ts +80 -0
  101. package/dist/timing.d.ts.map +1 -0
  102. package/dist/timing.js +121 -0
  103. package/dist/tool-call-buffer.d.ts +5 -5
  104. package/dist/tool-call-buffer.d.ts.map +1 -1
  105. package/dist/tool-call-buffer.js +28 -8
  106. package/dist/types-anthropic.d.ts +161 -1
  107. package/dist/types-anthropic.d.ts.map +1 -1
  108. package/dist/types.d.ts +172 -2
  109. package/dist/types.d.ts.map +1 -1
  110. package/package.json +15 -3
package/dist/errors.d.ts CHANGED
@@ -32,6 +32,7 @@ export declare function sendAnthropicError(res: ServerResponse, status: number,
32
32
  export declare function sendAnthropicBadRequest(res: ServerResponse, message: string): void;
33
33
  export declare function sendAnthropicNotFound(res: ServerResponse, message: string): void;
34
34
  export declare function sendAnthropicInternalError(res: ServerResponse, message: string): void;
35
+ export declare function sendAnthropicNotImplemented(res: ServerResponse, message: string): void;
35
36
  export declare function sendAnthropicMethodNotAllowed(res: ServerResponse, allowed: string): void;
36
37
  /**
37
38
  * 429 Anthropic-shape rate-limit response. Mirror of {@link sendRateLimit}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,SAAS,CACvB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACpB,IAAI,CAWN;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEvE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAO/E;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAE5E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAYxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAG3G;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAElF;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEhF;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAErF;AAED,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGxF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGjF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,SAAS,CACvB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACpB,IAAI,CAWN;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEvE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAO/E;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAE5E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAYxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAG3G;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAElF;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEhF;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAErF;AAED,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEtF;AAED,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGxF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGjF"}
package/dist/errors.js CHANGED
@@ -68,6 +68,9 @@ export function sendAnthropicNotFound(res, message) {
68
68
  export function sendAnthropicInternalError(res, message) {
69
69
  sendAnthropicError(res, 500, 'api_error', message);
70
70
  }
71
+ export function sendAnthropicNotImplemented(res, message) {
72
+ sendAnthropicError(res, 501, 'not_supported_error', message);
73
+ }
71
74
  export function sendAnthropicMethodNotAllowed(res, allowed) {
72
75
  res.writeHead(405, { Allow: allowed, 'Content-Type': 'application/json' });
73
76
  res.end(JSON.stringify({ type: 'error', error: { type: 'invalid_request_error', message: 'Method not allowed' } }));
package/dist/handler.d.ts CHANGED
@@ -1,9 +1,31 @@
1
1
  /** Composable `(req, res)` handler for node:http — usable standalone or mounted into an existing server. */
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http';
3
3
  import type { ResponseStore } from '@mlx-node/core';
4
+ import { type ServerHealth } from './health.js';
5
+ import type { IdleSweeper } from './idle-sweeper.js';
6
+ import { ModelWorkCoordinator } from './model-work-coordinator.js';
4
7
  import type { ModelRegistry } from './registry.js';
8
+ /**
9
+ * Entry in the list returned by `GET /v1/models`. Matches the shape
10
+ * `ModelRegistry.list()` produces — exported so callers that supply a
11
+ * custom `listModels` callback (e.g. `mlx launch claude` discovering
12
+ * every model on disk) can build entries without importing internals.
13
+ */
14
+ export interface PublicModelEntry {
15
+ id: string;
16
+ object: 'model';
17
+ created: number;
18
+ owned_by: string;
19
+ }
5
20
  export interface HandlerOptions {
6
- /** Enable CORS headers (default: true). */
21
+ /**
22
+ * Enable CORS headers.
23
+ *
24
+ * Default: `true` when no `authToken` is set (historical behaviour), and
25
+ * `false` once one is — `Access-Control-Allow-Origin: *` on a
26
+ * token-protected server would invite any web page to spend a leaked token
27
+ * from the user's browser. An explicit value always wins.
28
+ */
7
29
  cors?: boolean;
8
30
  /** Response store for previous_response_id support. */
9
31
  store?: ResponseStore | null;
@@ -13,6 +35,52 @@ export interface HandlerOptions {
13
35
  * and `ServerConfig.responseRetentionSec`).
14
36
  */
15
37
  responseRetentionSec?: number;
38
+ /**
39
+ * Optional idle sweeper. Forwarded to `routeRequest` so that the
40
+ * inference endpoints (`/v1/responses` + `/v1/messages`) can bracket
41
+ * their native-model dispatch with `beginRequest()` / `endRequest()`.
42
+ *
43
+ * Note: the begin/end hooks are DELIBERATELY scoped to the inference
44
+ * endpoints — wrapping every HTTP call (OPTIONS preflights,
45
+ * `/v1/models`, `/v1/health`, 404s) would let purely observational
46
+ * traffic keep the allocator pinned forever.
47
+ */
48
+ idleSweeper?: IdleSweeper | null;
49
+ /**
50
+ * Optional async callback invoked by `/v1/messages` before it looks
51
+ * the model up in the registry. The callback should register the
52
+ * model on demand; on return, the endpoint does `registry.get(name)`
53
+ * and 404s if still unresolved.
54
+ */
55
+ resolveModel?: (name: string) => Promise<void>;
56
+ /**
57
+ * Coordinates process-wide MLX work so lazy model loads / warmups do not
58
+ * overlap live inference on another model.
59
+ */
60
+ modelWorkCoordinator?: ModelWorkCoordinator;
61
+ /**
62
+ * Optional override for `GET /v1/models`. When provided, that endpoint
63
+ * returns this list instead of `registry.list()`.
64
+ */
65
+ listModels?: () => PublicModelEntry[];
66
+ /**
67
+ * Shared secret required on every route except `/health` and `/v1/health`.
68
+ *
69
+ * `undefined` (the default) disables the gate entirely and is byte-for-byte
70
+ * identical to the pre-auth behaviour: no header is inspected, no
71
+ * `WWW-Authenticate` is emitted, and CORS keeps its `true` default.
72
+ *
73
+ * Accepted as `x-api-key: <token>` (checked first — Anthropic clients send
74
+ * it) or `authorization: Bearer <token>` (scheme case-insensitive).
75
+ */
76
+ authToken?: string;
77
+ /**
78
+ * Builds the `/health` body. Supplied by `createServer` so the HTTP
79
+ * endpoint and `ServerInstance.health()` share one uptime origin. When
80
+ * omitted, `createHandler` builds its own reporter from `registry`,
81
+ * `idleSweeper` and `modelWorkCoordinator`.
82
+ */
83
+ health?: () => ServerHealth;
16
84
  }
17
85
  export declare function createHandler(registry: ModelRegistry, options?: HandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
18
86
  //# sourceMappingURL=handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAE5G,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uDAAuD;IACvD,KAAK,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,cAAc,GACvB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAgC9D"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAE5G,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIpD,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAoBnD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uDAAuD;IACvD,KAAK,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,gBAAgB,EAAE,CAAC;IACtC;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,YAAY,CAAC;CAC7B;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,cAAc,GACvB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAgF9D"}
package/dist/handler.js CHANGED
@@ -1,26 +1,80 @@
1
1
  /** Composable `(req, res)` handler for node:http — usable standalone or mounted into an existing server. */
2
+ import { hasCredential, isAuthorized, sendUnauthorized } from './auth.js';
2
3
  import { sendInternalError } from './errors.js';
3
- import { routeRequest } from './router.js';
4
+ import { createHealthReporter } from './health.js';
5
+ import { ModelWorkCoordinator } from './model-work-coordinator.js';
6
+ import { requestPathname, routeRequest } from './router.js';
7
+ /**
8
+ * Routes reachable WITHOUT a token when one is configured.
9
+ *
10
+ * A supervisor has to poll liveness before it can be handed a token (it may
11
+ * be the thing that generates it). Everything served here is scrubbed to
12
+ * `{ status, uptimeMs, pid }` by the router when the caller is
13
+ * unauthenticated — see `toMinimalHealth`.
14
+ *
15
+ * `/` is included because it is a pure liveness stub: the router answers it
16
+ * with a constant `{ service: 'mlx-node' }` and reads no state, so there is
17
+ * nothing to leak. Claude Code issues `HEAD /` before its first request — it
18
+ * does send `x-api-key`, so it would pass the check anyway, but gating a
19
+ * contentless probe would 401 every other client's liveness check for no
20
+ * security gain.
21
+ */
22
+ const UNAUTHENTICATED_PATHS = new Set(['/', '/health', '/v1/health']);
4
23
  export function createHandler(registry, options) {
5
- const cors = options?.cors ?? true;
24
+ const authToken = options?.authToken;
25
+ // CORS defaults follow the auth posture; an explicit value still wins.
26
+ const cors = options?.cors ?? authToken === undefined;
6
27
  const store = options?.store ?? null;
7
28
  const responseRetentionSec = options?.responseRetentionSec;
29
+ const idleSweeper = options?.idleSweeper ?? null;
30
+ const resolveModel = options?.resolveModel;
31
+ const modelWorkCoordinator = options?.modelWorkCoordinator ?? (resolveModel ? new ModelWorkCoordinator() : undefined);
32
+ const listModels = options?.listModels;
33
+ const health = options?.health ?? createHealthReporter({ registry, idleSweeper, modelWorkCoordinator: modelWorkCoordinator });
8
34
  return async (req, res) => {
9
- if (cors) {
10
- res.setHeader('Access-Control-Allow-Origin', '*');
11
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
12
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version');
13
- if (req.method === 'OPTIONS') {
14
- res.writeHead(204);
15
- res.end();
16
- return;
17
- }
18
- }
19
- // Returning the promise lets tests await the full lifecycle including
20
- // post-`res.end()` bookkeeping (e.g. `SessionRegistry.adopt`). `http.createServer`
21
- // ignores the return value, so this is transparent to production callers.
35
+ // The guard spans the WHOLE listener, not just `routeRequest`. `http.createServer`
36
+ // discards the returned promise, so anything that escapes here is an unhandled
37
+ // rejection — and Node's default `--unhandled-rejections=throw` turns that into
38
+ // process death. A crash-by-request is the one failure a request handler must
39
+ // not have, so nothing before the routing call gets to be outside the try either.
22
40
  try {
23
- await routeRequest(req, res, registry, store, responseRetentionSec);
41
+ if (cors) {
42
+ res.setHeader('Access-Control-Allow-Origin', '*');
43
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
44
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version');
45
+ if (req.method === 'OPTIONS') {
46
+ res.writeHead(204);
47
+ res.end();
48
+ return;
49
+ }
50
+ }
51
+ // Single auth choke point. `routeRequest` is called from exactly one
52
+ // place (below), so no route can be added that bypasses this.
53
+ //
54
+ // `authToken === undefined` short-circuits before any header is touched:
55
+ // an unprotected server behaves exactly as it did before auth existed.
56
+ let authenticated = true;
57
+ if (authToken !== undefined) {
58
+ authenticated = isAuthorized(req, authToken);
59
+ if (!authenticated) {
60
+ // Host-independent: see `requestPathname`. Building the base from the
61
+ // `Host` header threw on `Host: [`, from a branch only an
62
+ // UNAUTHENTICATED request reaches — so enabling auth was what made the
63
+ // server killable by anyone who could open the socket.
64
+ const path = requestPathname(req);
65
+ // `/health` degrades to a liveness-only body instead of 401 — but
66
+ // ONLY when no credential was offered. A caller who presented a
67
+ // WRONG token gets the 401 it needs to notice the typo.
68
+ if (!UNAUTHENTICATED_PATHS.has(path) || hasCredential(req)) {
69
+ sendUnauthorized(res);
70
+ return;
71
+ }
72
+ }
73
+ }
74
+ // Returning the promise lets tests await the full lifecycle including
75
+ // post-`res.end()` bookkeeping (e.g. `SessionRegistry.adopt`). `http.createServer`
76
+ // ignores the return value, so this is transparent to production callers.
77
+ await routeRequest(req, res, registry, store, responseRetentionSec, idleSweeper, resolveModel, listModels, modelWorkCoordinator, { health, authenticated });
24
78
  }
25
79
  catch (err) {
26
80
  const message = err instanceof Error ? err.message : 'Internal server error';
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Server readiness reporting for supervisors (e.g. an Electron app running
3
+ * the inference server as a child process).
4
+ *
5
+ * The old `/health` returned a constant `{ status: 'ok' }`, which cannot
6
+ * distinguish four states a supervisor genuinely needs to tell apart:
7
+ *
8
+ * - up, nothing loaded → keep waiting, this is normal
9
+ * - up, model resident → route traffic
10
+ * - wedged mid-load → keep waiting, do NOT restart
11
+ * - wedged behind a full queue → shed load / warn the user
12
+ *
13
+ * Every input already exists in-process; this module only exposes it. The
14
+ * status ladder itself is a PURE function ({@link deriveHealthStatus}) over a
15
+ * plain record so it can be unit-tested without a server, a registry, or the
16
+ * native addon.
17
+ *
18
+ * IMPORTANT: nothing here may call into `@mlx-node/core`. `/health` is
19
+ * deliberately excluded from the idle sweeper's `beginRequest`/`endRequest`
20
+ * bracket (see `HandlerOptions.idleSweeper`), so a native call from this path
21
+ * would run outside the in-flight accounting the drain timer relies on.
22
+ * Every field below is read from plain JavaScript state.
23
+ */
24
+ import type { IdleSweeper } from './idle-sweeper.js';
25
+ import type { ModelWorkCoordinator } from './model-work-coordinator.js';
26
+ import type { ModelRegistry } from './registry.js';
27
+ /**
28
+ * Coarse readiness classification. Ordered by precedence in
29
+ * {@link deriveHealthStatus} — a higher rung wins even when a lower rung's
30
+ * condition also holds.
31
+ */
32
+ export type ServerHealthStatus = 'ok' | 'loading' | 'degraded' | 'error';
33
+ /**
34
+ * Outcome of the most recent load bracket, recorded by
35
+ * {@link ModelWorkCoordinator} in the `finally` of `withModelLoad` /
36
+ * `withModelLoadInstrumented`.
37
+ *
38
+ * Before this existed, a `resolveModel` failure became an HTTP 500 and was
39
+ * dropped — a supervisor polling after the fact had no way to learn WHY the
40
+ * server had no resident model.
41
+ *
42
+ * Caveat worth knowing when reading this field: the coordinator brackets
43
+ * EVERY resolve attempt, including the no-op fast path taken when the
44
+ * requested model is already resident. A successful no-op therefore
45
+ * overwrites an earlier failure record. That is why the `'error'` rung is
46
+ * additionally gated on "no resident models" — a server that is answering
47
+ * requests is not in an error state regardless of what the last bracket did.
48
+ */
49
+ export interface ModelLoadRecord {
50
+ /** Caller-supplied label, normally the model name. `null` when unlabelled. */
51
+ label: string | null;
52
+ /** `Date.now()` at writer-lock acquisition (when the load actually began). */
53
+ startedAt: number;
54
+ /** `Date.now()` when the bracket settled, success or failure. */
55
+ finishedAt: number;
56
+ ok: boolean;
57
+ /** Message of the thrown error, or `null` on success. */
58
+ error: string | null;
59
+ }
60
+ /** Inputs to the pure status ladder. Deliberately primitive so tests can fixture them. */
61
+ export interface HealthStatusInputs {
62
+ /** True while a load holds the coordinator's exclusive writer slot. */
63
+ writerActive: boolean;
64
+ /** Loads parked waiting for the writer slot. */
65
+ waitingWriters: number;
66
+ /** Inference requests currently bracketed by the idle sweeper. */
67
+ inFlight: number;
68
+ /** Distinct model names currently registered. */
69
+ residentModelCount: number;
70
+ /** True when at least one per-model queue is holding its configured max waiters. */
71
+ queueSaturated: boolean;
72
+ /** Most recent load bracket, or `null` if none has settled. */
73
+ lastLoad: ModelLoadRecord | null;
74
+ }
75
+ /** Full readiness body. `{ status: 'ok' }` remains a strict subset of this. */
76
+ export interface ServerHealth {
77
+ status: ServerHealthStatus;
78
+ /** Milliseconds since the reporter was created (≈ server start). */
79
+ uptimeMs: number;
80
+ pid: number;
81
+ models: {
82
+ /** Registered names, including aliases. */
83
+ resident: string[];
84
+ count: number;
85
+ };
86
+ work: {
87
+ inFlight: number;
88
+ /** True while the idle sweeper has a `clearCache()` drain armed. */
89
+ drainPending: boolean;
90
+ writerActive: boolean;
91
+ waitingWriters: number;
92
+ };
93
+ queue: {
94
+ /** Deepest per-model waiter count across every session registry. */
95
+ depth: number;
96
+ /** Configured per-model waiter cap, or `null` when unbounded. */
97
+ limit: number | null;
98
+ saturated: boolean;
99
+ };
100
+ lastLoad: ModelLoadRecord | null;
101
+ }
102
+ /**
103
+ * The subset served to an UNAUTHENTICATED `/health` poll on a
104
+ * token-protected server. A supervisor must be able to poll before it holds a
105
+ * token, but `models.resident` is user data — model names routinely leak
106
+ * project names and local paths.
107
+ */
108
+ export interface ServerHealthMinimal {
109
+ status: ServerHealthStatus;
110
+ uptimeMs: number;
111
+ pid: number;
112
+ }
113
+ /**
114
+ * Pure status ladder. No I/O, no clock, no native calls — just the four
115
+ * documented rungs, in precedence order:
116
+ *
117
+ * 1. `writerActive` → 'loading'
118
+ * 2. last load failed AND nothing resident → 'error'
119
+ * 3. queue saturated, OR a load is parked
120
+ * behind live inference → 'degraded'
121
+ * 4. otherwise → 'ok'
122
+ *
123
+ * `'loading'` outranks `'error'` on purpose: a retry that already holds the
124
+ * writer slot means the supervisor should wait, not restart the process.
125
+ */
126
+ export declare function deriveHealthStatus(input: HealthStatusInputs): ServerHealthStatus;
127
+ /** Project the full body down to the three fields safe to serve without a token. */
128
+ export declare function toMinimalHealth(health: ServerHealth): ServerHealthMinimal;
129
+ export interface HealthReporterDeps {
130
+ registry: ModelRegistry;
131
+ /** Optional: supplies `inFlight` / `drainPending`. Absent ⇒ both read as idle. */
132
+ idleSweeper?: IdleSweeper | null;
133
+ /** Optional: supplies writer state + `lastLoad`. Absent ⇒ no load has ever run. */
134
+ modelWorkCoordinator?: ModelWorkCoordinator | null;
135
+ /** Defaults to `Date.now()` at construction. */
136
+ startedAt?: number;
137
+ /** Injectable clock for tests. */
138
+ now?: () => number;
139
+ }
140
+ /**
141
+ * Build a zero-argument reporter closing over the live server objects. Each
142
+ * call re-reads current state; nothing is cached, so a supervisor polling on
143
+ * an interval always sees the present moment.
144
+ */
145
+ export declare function createHealthReporter(deps: HealthReporterDeps): () => ServerHealth;
146
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;AAEzE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,0FAA0F;AAC1F,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,YAAY,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,cAAc,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oFAAoF;IACpF,cAAc,EAAE,OAAO,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CAClC;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE;QACN,2CAA2C;QAC3C,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,oEAAoE;QACpE,YAAY,EAAE,OAAO,CAAC;QACtB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,KAAK,EAAE;QACL,oEAAoE;QACpE,KAAK,EAAE,MAAM,CAAC;QACd,iEAAiE;QACjE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CAClC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,kBAAkB,CAShF;AAED,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,mBAAmB,CAEzE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,aAAa,CAAC;IACxB,kFAAkF;IAClF,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACnD,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,YAAY,CAiDjF"}
package/dist/health.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Server readiness reporting for supervisors (e.g. an Electron app running
3
+ * the inference server as a child process).
4
+ *
5
+ * The old `/health` returned a constant `{ status: 'ok' }`, which cannot
6
+ * distinguish four states a supervisor genuinely needs to tell apart:
7
+ *
8
+ * - up, nothing loaded → keep waiting, this is normal
9
+ * - up, model resident → route traffic
10
+ * - wedged mid-load → keep waiting, do NOT restart
11
+ * - wedged behind a full queue → shed load / warn the user
12
+ *
13
+ * Every input already exists in-process; this module only exposes it. The
14
+ * status ladder itself is a PURE function ({@link deriveHealthStatus}) over a
15
+ * plain record so it can be unit-tested without a server, a registry, or the
16
+ * native addon.
17
+ *
18
+ * IMPORTANT: nothing here may call into `@mlx-node/core`. `/health` is
19
+ * deliberately excluded from the idle sweeper's `beginRequest`/`endRequest`
20
+ * bracket (see `HandlerOptions.idleSweeper`), so a native call from this path
21
+ * would run outside the in-flight accounting the drain timer relies on.
22
+ * Every field below is read from plain JavaScript state.
23
+ */
24
+ /**
25
+ * Pure status ladder. No I/O, no clock, no native calls — just the four
26
+ * documented rungs, in precedence order:
27
+ *
28
+ * 1. `writerActive` → 'loading'
29
+ * 2. last load failed AND nothing resident → 'error'
30
+ * 3. queue saturated, OR a load is parked
31
+ * behind live inference → 'degraded'
32
+ * 4. otherwise → 'ok'
33
+ *
34
+ * `'loading'` outranks `'error'` on purpose: a retry that already holds the
35
+ * writer slot means the supervisor should wait, not restart the process.
36
+ */
37
+ export function deriveHealthStatus(input) {
38
+ if (input.writerActive)
39
+ return 'loading';
40
+ if (input.lastLoad?.ok === false && input.residentModelCount === 0)
41
+ return 'error';
42
+ if (input.queueSaturated)
43
+ return 'degraded';
44
+ // A writer parked while readers are still running means the swap cannot
45
+ // proceed until they drain, and every request for the incoming model
46
+ // stalls behind it. Either condition alone is an ordinary transient.
47
+ if (input.waitingWriters > 0 && input.inFlight > 0)
48
+ return 'degraded';
49
+ return 'ok';
50
+ }
51
+ /** Project the full body down to the three fields safe to serve without a token. */
52
+ export function toMinimalHealth(health) {
53
+ return { status: health.status, uptimeMs: health.uptimeMs, pid: health.pid };
54
+ }
55
+ /**
56
+ * Build a zero-argument reporter closing over the live server objects. Each
57
+ * call re-reads current state; nothing is cached, so a supervisor polling on
58
+ * an interval always sees the present moment.
59
+ */
60
+ export function createHealthReporter(deps) {
61
+ const now = deps.now ?? (() => Date.now());
62
+ const startedAt = deps.startedAt ?? now();
63
+ return () => {
64
+ const resident = deps.registry.list().map((entry) => entry.id);
65
+ // Queue saturation is inherently per-model: one wedged model should
66
+ // surface even while others are idle. We report the WORST case.
67
+ let depth = 0;
68
+ let limit = null;
69
+ let saturated = false;
70
+ for (const sessionRegistry of deps.registry.listSessionRegistries()) {
71
+ const registryDepth = sessionRegistry.queueDepth;
72
+ if (registryDepth > depth)
73
+ depth = registryDepth;
74
+ const registryLimit = sessionRegistry.queueDepthLimit;
75
+ if (registryLimit !== undefined) {
76
+ if (limit === null || registryLimit < limit)
77
+ limit = registryLimit;
78
+ if (registryDepth >= registryLimit)
79
+ saturated = true;
80
+ }
81
+ }
82
+ const writerActive = deps.modelWorkCoordinator?.writerActive ?? false;
83
+ const waitingWriters = deps.modelWorkCoordinator?.waitingWriters ?? 0;
84
+ const lastLoad = deps.modelWorkCoordinator?.lastLoad ?? null;
85
+ const inFlight = deps.idleSweeper?.inFlight ?? 0;
86
+ const drainPending = deps.idleSweeper?.isPending ?? false;
87
+ const status = deriveHealthStatus({
88
+ writerActive,
89
+ waitingWriters,
90
+ inFlight,
91
+ residentModelCount: resident.length,
92
+ queueSaturated: saturated,
93
+ lastLoad,
94
+ });
95
+ return {
96
+ status,
97
+ // Clamped: a backwards clock step (NTP, fake timers in a sibling test)
98
+ // must not surface a negative uptime a supervisor might read as a wrap.
99
+ uptimeMs: Math.max(0, now() - startedAt),
100
+ pid: process.pid,
101
+ models: { resident, count: resident.length },
102
+ work: { inFlight, drainPending, writerActive, waitingWriters },
103
+ queue: { depth, limit, saturated },
104
+ lastLoad,
105
+ };
106
+ };
107
+ }
@@ -0,0 +1,19 @@
1
+ /** Discover locally-downloaded generative models under a given directory. */
2
+ import { type ModelType } from '@mlx-node/lm';
3
+ import { type LaunchPreset } from '../presets.js';
4
+ /** A locally-downloaded model paired with its sampling preset. */
5
+ export interface DiscoveredModel {
6
+ name: string;
7
+ path: string;
8
+ modelType: ModelType;
9
+ preset: LaunchPreset;
10
+ }
11
+ /**
12
+ * Scan `dir` for model subdirectories. Each subdirectory with a recognized
13
+ * `config.json` is returned with its inferred `ModelType` and `LaunchPreset`.
14
+ * Non-generative types are silently skipped. Entries with no preset or an
15
+ * undetectable config are skipped (warnings only emitted when `MLX_DEBUG`
16
+ * is set). Must stay cheap — do not load weights here.
17
+ */
18
+ export declare function discoverModels(dir: string): Promise<DiscoveredModel[]>;
19
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/host/discover.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAM7E,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAElE,kEAAkE;AAClE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;CACtB;AAKD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAoC5E"}
@@ -0,0 +1,50 @@
1
+ /** Discover locally-downloaded generative models under a given directory. */
2
+ import { readdir } from 'node:fs/promises';
3
+ import { basename, join } from 'node:path';
4
+ import { detectModelType } from '@mlx-node/lm';
5
+ import { LAUNCH_PRESETS } from '../presets.js';
6
+ // Non-generative detection results that cannot back a chat endpoint.
7
+ const NON_GENERATIVE = new Set(['harrier', 'qianfan-ocr', 'internvl_chat']);
8
+ /**
9
+ * Scan `dir` for model subdirectories. Each subdirectory with a recognized
10
+ * `config.json` is returned with its inferred `ModelType` and `LaunchPreset`.
11
+ * Non-generative types are silently skipped. Entries with no preset or an
12
+ * undetectable config are skipped (warnings only emitted when `MLX_DEBUG`
13
+ * is set). Must stay cheap — do not load weights here.
14
+ */
15
+ export async function discoverModels(dir) {
16
+ const debug = Boolean(process.env.MLX_DEBUG);
17
+ let entries;
18
+ try {
19
+ entries = await readdir(dir, { withFileTypes: true });
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ const out = [];
25
+ for (const entry of entries) {
26
+ if (!entry.isDirectory())
27
+ continue;
28
+ const full = join(dir, entry.name);
29
+ let modelType;
30
+ try {
31
+ modelType = await detectModelType(full);
32
+ }
33
+ catch (err) {
34
+ if (debug)
35
+ console.warn(`[mlx] skip ${full}: ${err.message}`);
36
+ continue;
37
+ }
38
+ if (NON_GENERATIVE.has(modelType))
39
+ continue;
40
+ const preset = LAUNCH_PRESETS[modelType];
41
+ if (!preset) {
42
+ if (debug)
43
+ console.warn(`[mlx] skip ${full}: no LAUNCH_PRESETS entry for ${modelType}`);
44
+ continue;
45
+ }
46
+ out.push({ name: basename(full), path: full, modelType, preset });
47
+ }
48
+ out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
49
+ return out;
50
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Engine tuning that is LAUNCHER POLICY, not an engine default.
3
+ *
4
+ * The native engine reads these knobs from the process environment through a
5
+ * `OnceLock`: the FIRST read latches the value for the life of the process, so
6
+ * every variable here must be in place before any model is loaded and must
7
+ * never be mutated afterwards. That one-shot latch is why the policy is a
8
+ * plain data object rather than something a caller can toggle at runtime.
9
+ *
10
+ * Two application shapes, deliberately different:
11
+ *
12
+ * - In-process host (`mlx serve`, `mlx launch claude`): call
13
+ * {@link applyEnginePolicy} to write `process.env` BEFORE the first load.
14
+ * A value the user already set in their shell always wins — this is a
15
+ * default, not an override.
16
+ * - Out-of-process host (Electron `utilityProcess.fork`): pass
17
+ * {@link engineEnvFor} into `fork({ env })` and never touch
18
+ * `process.env` in the child. The returned map is unconditional: it
19
+ * describes the child's starting environment, and the parent is expected
20
+ * to spread the user's own env under it if it wants shell values to win.
21
+ *
22
+ * Keeping the two apart matters. `MLX_PAGED_PREFILL_CHUNK_SIZE` reads as
23
+ * `0` ("no chunking") in Rust when unset; 2048 is the value
24
+ * `mlx launch claude` has historically applied to bound the cold-prefill
25
+ * memory peak, and baking it into the engine would silently change every
26
+ * other embedder's behaviour.
27
+ */
28
+ /** Env var names this module owns. Exported so tests can assert the exact set. */
29
+ export declare const ENGINE_POLICY_ENV_VARS: readonly ['MLX_PAGED_PREFILL_CHUNK_SIZE'];
30
+ export interface EnginePolicy {
31
+ /**
32
+ * `MLX_PAGED_PREFILL_CHUNK_SIZE` — tokens per paged-prefill chunk.
33
+ * `0` disables chunking (the Rust default when the var is unset).
34
+ */
35
+ pagedPrefillChunkSize?: number;
36
+ }
37
+ /**
38
+ * The policy `mlx launch claude` has applied since the flag existed, and the
39
+ * one `mlx serve` and the desktop sidecar inherit.
40
+ *
41
+ * 2048 matches the mlx-lm / mlx-vlm default and reduces per-chunk overhead
42
+ * versus the older 1024 for long Qwen dense contexts.
43
+ */
44
+ export declare const LAUNCHER_ENGINE_POLICY: EnginePolicy;
45
+ /**
46
+ * Render a policy as the environment map a forked child should start with.
47
+ *
48
+ * Unconditional by construction: there is no "already set" to respect in a
49
+ * child that does not exist yet.
50
+ */
51
+ export declare function engineEnvFor(policy: EnginePolicy): Record<string, string>;
52
+ /**
53
+ * Apply a policy to an in-process environment as a DEFAULT.
54
+ *
55
+ * Only writes vars that are currently unset — matching the historical
56
+ * `if (process.env.X == null)` guard, so an explicit `X=` (empty string) in
57
+ * the user's shell is treated as "the user has an opinion" and left alone.
58
+ *
59
+ * Returns the names actually written, so a caller can log or assert on them.
60
+ */
61
+ export declare function applyEnginePolicy(policy: EnginePolicy, env?: NodeJS.ProcessEnv): string[];
62
+ //# sourceMappingURL=env-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-policy.d.ts","sourceRoot":"","sources":["../../src/host/env-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,YAAI,8BAA8B,CAAU,CAAC;AAEhF,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,YAA6D,CAAC;AAEnG;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAStG"}