@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.
- package/dist/auth.d.ts +56 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +106 -0
- package/dist/chat-session-warm-reuse.d.ts +51 -0
- package/dist/chat-session-warm-reuse.d.ts.map +1 -0
- package/dist/chat-session-warm-reuse.js +72 -0
- package/dist/endpoints/messages-count-tokens.d.ts +8 -0
- package/dist/endpoints/messages-count-tokens.d.ts.map +1 -0
- package/dist/endpoints/messages-count-tokens.js +121 -0
- package/dist/endpoints/messages.d.ts +57 -5
- package/dist/endpoints/messages.d.ts.map +1 -1
- package/dist/endpoints/messages.js +1043 -147
- package/dist/endpoints/models.d.ts +2 -1
- package/dist/endpoints/models.d.ts.map +1 -1
- package/dist/endpoints/models.js +2 -2
- package/dist/endpoints/responses.d.ts +22 -7
- package/dist/endpoints/responses.d.ts.map +1 -1
- package/dist/endpoints/responses.js +608 -85
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/handler.d.ts +69 -1
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +70 -16
- package/dist/health.d.ts +146 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +107 -0
- package/dist/host/discover.d.ts +19 -0
- package/dist/host/discover.d.ts.map +1 -0
- package/dist/host/discover.js +50 -0
- package/dist/host/env-policy.d.ts +62 -0
- package/dist/host/env-policy.d.ts.map +1 -0
- package/dist/host/env-policy.js +69 -0
- package/dist/host/index.d.ts +202 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +325 -0
- package/dist/host/logger.d.ts +36 -0
- package/dist/host/logger.d.ts.map +1 -0
- package/dist/host/logger.js +376 -0
- package/dist/host/net.d.ts +65 -0
- package/dist/host/net.d.ts.map +1 -0
- package/dist/host/net.js +97 -0
- package/dist/host/paths.d.ts +28 -0
- package/dist/host/paths.d.ts.map +1 -0
- package/dist/host/paths.js +71 -0
- package/dist/host/swap.d.ts +27 -0
- package/dist/host/swap.d.ts.map +1 -0
- package/dist/host/swap.js +178 -0
- package/dist/host/temp-root.d.ts +57 -0
- package/dist/host/temp-root.d.ts.map +1 -0
- package/dist/host/temp-root.js +99 -0
- package/dist/idle-sweeper.d.ts +245 -0
- package/dist/idle-sweeper.d.ts.map +1 -0
- package/dist/idle-sweeper.js +408 -0
- package/dist/index.d.ts +19 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/load-model.d.ts +69 -0
- package/dist/load-model.d.ts.map +1 -0
- package/dist/load-model.js +63 -0
- package/dist/mappers/anthropic-request.d.ts +24 -2
- package/dist/mappers/anthropic-request.d.ts.map +1 -1
- package/dist/mappers/anthropic-request.js +222 -24
- package/dist/mappers/anthropic-response.d.ts +29 -4
- package/dist/mappers/anthropic-response.d.ts.map +1 -1
- package/dist/mappers/anthropic-response.js +143 -21
- package/dist/mappers/request.d.ts +48 -0
- package/dist/mappers/request.d.ts.map +1 -1
- package/dist/mappers/request.js +211 -35
- package/dist/mappers/response.d.ts.map +1 -1
- package/dist/mappers/response.js +13 -1
- package/dist/model-work-coordinator.d.ts +95 -0
- package/dist/model-work-coordinator.d.ts.map +1 -0
- package/dist/model-work-coordinator.js +201 -0
- package/dist/pending-writes.d.ts.map +1 -1
- package/dist/presets.d.ts +82 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +98 -0
- package/dist/registry.d.ts +31 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +33 -5
- package/dist/router.d.ts +37 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +79 -7
- package/dist/server.d.ts +193 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +173 -10
- package/dist/session-registry.d.ts +279 -18
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +518 -37
- package/dist/stop-sequence-buffer.d.ts +58 -0
- package/dist/stop-sequence-buffer.d.ts.map +1 -0
- package/dist/stop-sequence-buffer.js +148 -0
- package/dist/streaming.d.ts +14 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +45 -0
- package/dist/text-recovery.d.ts +35 -0
- package/dist/text-recovery.d.ts.map +1 -0
- package/dist/text-recovery.js +41 -0
- package/dist/timing.d.ts +80 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +121 -0
- package/dist/tool-call-buffer.d.ts +5 -5
- package/dist/tool-call-buffer.d.ts.map +1 -1
- package/dist/tool-call-buffer.js +28 -8
- package/dist/types-anthropic.d.ts +161 -1
- package/dist/types-anthropic.d.ts.map +1 -1
- package/dist/types.d.ts +172 -2
- package/dist/types.d.ts.map +1 -1
- 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}
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
package/dist/handler.d.ts.map
CHANGED
|
@@ -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;
|
|
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 {
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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';
|
package/dist/health.d.ts
ADDED
|
@@ -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"}
|