@mlx-node/server 0.0.8 → 0.0.10

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 (61) 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 +8 -8
  5. package/dist/chat-session-warm-reuse.d.ts.map +1 -1
  6. package/dist/chat-session-warm-reuse.js +12 -8
  7. package/dist/endpoints/responses.d.ts +3 -1
  8. package/dist/endpoints/responses.d.ts.map +1 -1
  9. package/dist/endpoints/responses.js +38 -5
  10. package/dist/handler.d.ts +27 -1
  11. package/dist/handler.d.ts.map +1 -1
  12. package/dist/handler.js +65 -16
  13. package/dist/health.d.ts +146 -0
  14. package/dist/health.d.ts.map +1 -0
  15. package/dist/health.js +107 -0
  16. package/dist/host/discover.d.ts +19 -0
  17. package/dist/host/discover.d.ts.map +1 -0
  18. package/dist/host/discover.js +50 -0
  19. package/dist/host/env-policy.d.ts +62 -0
  20. package/dist/host/env-policy.d.ts.map +1 -0
  21. package/dist/host/env-policy.js +69 -0
  22. package/dist/host/index.d.ts +202 -0
  23. package/dist/host/index.d.ts.map +1 -0
  24. package/dist/host/index.js +325 -0
  25. package/dist/host/logger.d.ts +36 -0
  26. package/dist/host/logger.d.ts.map +1 -0
  27. package/dist/host/logger.js +376 -0
  28. package/dist/host/net.d.ts +65 -0
  29. package/dist/host/net.d.ts.map +1 -0
  30. package/dist/host/net.js +97 -0
  31. package/dist/host/paths.d.ts +28 -0
  32. package/dist/host/paths.d.ts.map +1 -0
  33. package/dist/host/paths.js +71 -0
  34. package/dist/host/swap.d.ts +27 -0
  35. package/dist/host/swap.d.ts.map +1 -0
  36. package/dist/host/swap.js +178 -0
  37. package/dist/host/temp-root.d.ts +57 -0
  38. package/dist/host/temp-root.d.ts.map +1 -0
  39. package/dist/host/temp-root.js +99 -0
  40. package/dist/index.d.ts +11 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7 -1
  43. package/dist/load-model.d.ts +69 -0
  44. package/dist/load-model.d.ts.map +1 -0
  45. package/dist/load-model.js +63 -0
  46. package/dist/model-work-coordinator.d.ts +29 -4
  47. package/dist/model-work-coordinator.d.ts.map +1 -1
  48. package/dist/model-work-coordinator.js +97 -16
  49. package/dist/router.d.ts +34 -1
  50. package/dist/router.d.ts.map +1 -1
  51. package/dist/router.js +47 -5
  52. package/dist/server.d.ts +117 -3
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +125 -9
  55. package/dist/session-registry.d.ts +7 -0
  56. package/dist/session-registry.d.ts.map +1 -1
  57. package/dist/session-registry.js +9 -0
  58. package/dist/streaming.d.ts +14 -0
  59. package/dist/streaming.d.ts.map +1 -1
  60. package/dist/streaming.js +45 -0
  61. package/package.json +15 -3
package/dist/auth.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Optional bearer-token gate.
3
+ *
4
+ * Enforced at the single choke point in `createHandler`'s returned closure —
5
+ * after the CORS/OPTIONS early-return, before `routeRequest` — so there is
6
+ * exactly one place a route can be added without inheriting the gate.
7
+ *
8
+ * Scope, deliberately narrow: this is a shared-secret check for a server
9
+ * bound to loopback and supervised by a local app. It is NOT a
10
+ * multi-tenant auth system — there is one token, no rotation, no scopes,
11
+ * no per-caller identity.
12
+ */
13
+ import type { IncomingMessage, ServerResponse } from 'node:http';
14
+ /**
15
+ * Extract the presented credential, or `null` when none is usable.
16
+ *
17
+ * `x-api-key` is checked FIRST because Anthropic clients (including Claude
18
+ * Code, the primary consumer of `/v1/messages`) send it. Checking it first
19
+ * also means a stale `authorization` header injected by an intermediary
20
+ * cannot shadow the caller's real key.
21
+ *
22
+ * Array-valued headers are rejected outright. Node collapses repeated
23
+ * non-allowlisted headers such as `x-api-key` into a `string[]`; joining or
24
+ * picking one arbitrarily would let a caller smuggle a second value past a
25
+ * front proxy that only inspected the first.
26
+ */
27
+ export declare function extractPresentedToken(req: IncomingMessage): string | null;
28
+ /**
29
+ * Constant-time-ish credential comparison.
30
+ *
31
+ * The length check in front of `timingSafeEqual` is unavoidable — the
32
+ * primitive throws on mismatched lengths. It therefore LEAKS the length of
33
+ * the configured token to an attacker who can time responses. That is an
34
+ * accepted trade: the token is a locally-generated high-entropy secret, and
35
+ * knowing its length does not meaningfully reduce the search space. The
36
+ * byte-by-byte content comparison, which is the part that would otherwise
37
+ * allow incremental guessing, stays constant-time.
38
+ */
39
+ export declare function tokensMatch(presented: string, expected: string): boolean;
40
+ /**
41
+ * `true` when the caller offered SOME credential, valid or not.
42
+ *
43
+ * Used only by the `/health` carve-out: an anonymous poll degrades to a
44
+ * liveness-only body, but a caller who presented a wrong (or malformed)
45
+ * credential gets a 401 so a mistyped token surfaces instead of masquerading
46
+ * as a healthy 200.
47
+ */
48
+ export declare function hasCredential(req: IncomingMessage): boolean;
49
+ /** `true` when the request carries a credential matching `expected`. */
50
+ export declare function isAuthorized(req: IncomingMessage, expected: string): boolean;
51
+ /**
52
+ * 401 with `WWW-Authenticate: Bearer`. The body deliberately says nothing
53
+ * about whether a credential was absent, malformed, or simply wrong.
54
+ */
55
+ export declare function sendUnauthorized(res: ServerResponse): void;
56
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAKjE;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAYzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAaxE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAE3D;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI5E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAe1D"}
package/dist/auth.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Optional bearer-token gate.
3
+ *
4
+ * Enforced at the single choke point in `createHandler`'s returned closure —
5
+ * after the CORS/OPTIONS early-return, before `routeRequest` — so there is
6
+ * exactly one place a route can be added without inheriting the gate.
7
+ *
8
+ * Scope, deliberately narrow: this is a shared-secret check for a server
9
+ * bound to loopback and supervised by a local app. It is NOT a
10
+ * multi-tenant auth system — there is one token, no rotation, no scopes,
11
+ * no per-caller identity.
12
+ */
13
+ import { timingSafeEqual } from 'node:crypto';
14
+ /** `Bearer ` prefix length, used to slice the credential out of `authorization`. */
15
+ const BEARER_PREFIX = 'bearer ';
16
+ /**
17
+ * Extract the presented credential, or `null` when none is usable.
18
+ *
19
+ * `x-api-key` is checked FIRST because Anthropic clients (including Claude
20
+ * Code, the primary consumer of `/v1/messages`) send it. Checking it first
21
+ * also means a stale `authorization` header injected by an intermediary
22
+ * cannot shadow the caller's real key.
23
+ *
24
+ * Array-valued headers are rejected outright. Node collapses repeated
25
+ * non-allowlisted headers such as `x-api-key` into a `string[]`; joining or
26
+ * picking one arbitrarily would let a caller smuggle a second value past a
27
+ * front proxy that only inspected the first.
28
+ */
29
+ export function extractPresentedToken(req) {
30
+ const apiKey = req.headers['x-api-key'];
31
+ if (apiKey !== undefined) {
32
+ return typeof apiKey === 'string' ? apiKey : null;
33
+ }
34
+ const authorization = req.headers.authorization;
35
+ if (typeof authorization !== 'string')
36
+ return null;
37
+ // Scheme is case-insensitive per RFC 7235; the credential is not.
38
+ if (authorization.length <= BEARER_PREFIX.length)
39
+ return null;
40
+ if (authorization.slice(0, BEARER_PREFIX.length).toLowerCase() !== BEARER_PREFIX)
41
+ return null;
42
+ return authorization.slice(BEARER_PREFIX.length);
43
+ }
44
+ /**
45
+ * Constant-time-ish credential comparison.
46
+ *
47
+ * The length check in front of `timingSafeEqual` is unavoidable — the
48
+ * primitive throws on mismatched lengths. It therefore LEAKS the length of
49
+ * the configured token to an attacker who can time responses. That is an
50
+ * accepted trade: the token is a locally-generated high-entropy secret, and
51
+ * knowing its length does not meaningfully reduce the search space. The
52
+ * byte-by-byte content comparison, which is the part that would otherwise
53
+ * allow incremental guessing, stays constant-time.
54
+ */
55
+ export function tokensMatch(presented, expected) {
56
+ // The empty string is not a credential, in either position. `timingSafeEqual`
57
+ // on two zero-length buffers returns TRUE, so a server misconfigured with an
58
+ // empty token would authenticate every caller who sent an empty `x-api-key`.
59
+ // `resolveAuthToken` rejects an empty token before it can reach a server built
60
+ // through `createServer`; this is the second latch, for a caller that
61
+ // constructs `createHandler` directly. Refusing is the fail-closed direction —
62
+ // an empty token 401s everything rather than admitting everything.
63
+ if (presented === '' || expected === '')
64
+ return false;
65
+ const presentedBytes = Buffer.from(presented, 'utf8');
66
+ const expectedBytes = Buffer.from(expected, 'utf8');
67
+ if (presentedBytes.length !== expectedBytes.length)
68
+ return false;
69
+ return timingSafeEqual(presentedBytes, expectedBytes);
70
+ }
71
+ /**
72
+ * `true` when the caller offered SOME credential, valid or not.
73
+ *
74
+ * Used only by the `/health` carve-out: an anonymous poll degrades to a
75
+ * liveness-only body, but a caller who presented a wrong (or malformed)
76
+ * credential gets a 401 so a mistyped token surfaces instead of masquerading
77
+ * as a healthy 200.
78
+ */
79
+ export function hasCredential(req) {
80
+ return req.headers['x-api-key'] !== undefined || req.headers.authorization !== undefined;
81
+ }
82
+ /** `true` when the request carries a credential matching `expected`. */
83
+ export function isAuthorized(req, expected) {
84
+ const presented = extractPresentedToken(req);
85
+ if (presented === null)
86
+ return false;
87
+ return tokensMatch(presented, expected);
88
+ }
89
+ /**
90
+ * 401 with `WWW-Authenticate: Bearer`. The body deliberately says nothing
91
+ * about whether a credential was absent, malformed, or simply wrong.
92
+ */
93
+ export function sendUnauthorized(res) {
94
+ res.writeHead(401, {
95
+ 'WWW-Authenticate': 'Bearer realm="mlx-node"',
96
+ 'Content-Type': 'application/json',
97
+ });
98
+ res.end(JSON.stringify({
99
+ error: {
100
+ type: 'authentication_error',
101
+ message: 'Missing or invalid API key',
102
+ code: null,
103
+ param: null,
104
+ },
105
+ }));
106
+ }
@@ -22,14 +22,14 @@
22
22
  * native cache is correct there and only there.
23
23
  *
24
24
  * Fields accessed: `inFlight`, `history`, `lastImagesKey`, `lastAudioKey`, `turnCount`,
25
- * `unresolvedOkToolCallCount`, `needsFullReplay`. These are TypeScript `private` fields on
26
- * `ChatSession` (compile-time only) — at runtime they are ordinary
27
- * properties. The cast through {@link ChatSessionWarmReuseInternals}
28
- * gives this helper a typed view of the instance without relaxing the
29
- * class's `private` declarations. The field names MUST stay in sync
30
- * with `packages/lm/src/chat-session.ts`; a mismatch would silently
31
- * skip the intended state wipe and is covered by the chat-session
32
- * unit tests that exercise this path through the endpoint handler.
25
+ * `unresolvedOkToolCallCount`, `needsFullReplay`, `defaultConfig`, `activeTools`.
26
+ * These are TypeScript `private` fields on `ChatSession` (compile-time
27
+ * only) — at runtime they are ordinary properties. The cast through
28
+ * {@link ChatSessionWarmReuseInternals} gives this helper a typed view
29
+ * of the instance without relaxing the class's `private` declarations.
30
+ * The field names MUST stay in sync with
31
+ * `packages/lm/src/chat-session.ts`; a mismatch would silently skip
32
+ * the intended state wipe and is covered by the warm-reuse unit tests.
33
33
  */
34
34
  import type { ChatSession, SessionCapableModel } from '@mlx-node/lm';
35
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"chat-session-warm-reuse.d.ts","sourceRoot":"","sources":["../src/chat-session-warm-reuse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAmBrE;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,sCAAsC,CAAC,CAAC,SAAS,mBAAmB,EACxF,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
1
+ {"version":3,"file":"chat-session-warm-reuse.d.ts","sourceRoot":"","sources":["../src/chat-session-warm-reuse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAc,WAAW,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAqBjF;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,sCAAsC,CAAC,CAAC,SAAS,mBAAmB,EACxF,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -22,14 +22,14 @@
22
22
  * native cache is correct there and only there.
23
23
  *
24
24
  * Fields accessed: `inFlight`, `history`, `lastImagesKey`, `lastAudioKey`, `turnCount`,
25
- * `unresolvedOkToolCallCount`, `needsFullReplay`. These are TypeScript `private` fields on
26
- * `ChatSession` (compile-time only) — at runtime they are ordinary
27
- * properties. The cast through {@link ChatSessionWarmReuseInternals}
28
- * gives this helper a typed view of the instance without relaxing the
29
- * class's `private` declarations. The field names MUST stay in sync
30
- * with `packages/lm/src/chat-session.ts`; a mismatch would silently
31
- * skip the intended state wipe and is covered by the chat-session
32
- * unit tests that exercise this path through the endpoint handler.
25
+ * `unresolvedOkToolCallCount`, `needsFullReplay`, `defaultConfig`, `activeTools`.
26
+ * These are TypeScript `private` fields on `ChatSession` (compile-time
27
+ * only) — at runtime they are ordinary properties. The cast through
28
+ * {@link ChatSessionWarmReuseInternals} gives this helper a typed view
29
+ * of the instance without relaxing the class's `private` declarations.
30
+ * The field names MUST stay in sync with
31
+ * `packages/lm/src/chat-session.ts`; a mismatch would silently skip
32
+ * the intended state wipe and is covered by the warm-reuse unit tests.
33
33
  */
34
34
  /**
35
35
  * JS-state-only reset that DELIBERATELY preserves the underlying
@@ -65,4 +65,8 @@ export async function resetPreservingNativeCacheForWarmReuse(session) {
65
65
  internals.turnCount = 0;
66
66
  internals.unresolvedOkToolCallCount = null;
67
67
  internals.needsFullReplay = false;
68
+ // Tools are conversation state. A warm-any lease may belong to an
69
+ // unrelated request, so restore constructor defaults exactly like
70
+ // ChatSession.reset() instead of leaking the prior committed overlay.
71
+ internals.activeTools = internals.defaultConfig?.tools;
68
72
  }
@@ -88,5 +88,7 @@ export declare function __setServerBootIdForTesting(id: string): void;
88
88
  * `tool_result` / `tool_use_id`). Validation logic is identical.
89
89
  */
90
90
  export declare function validateAndCanonicalizeHistoryToolOrder(messages: ChatMessage[], apiSurface?: 'openai' | 'anthropic'): string | null;
91
- export declare function handleCreateResponse(res: ServerResponse, body: ResponsesAPIRequest, registry: ModelRegistry, store: ResponseStore | null, httpReq?: IncomingMessage, responseRetentionSec?: number, idleSweeper?: IdleSweeper | null, modelWorkCoordinator?: ModelWorkCoordinator): Promise<void>;
91
+ export declare function handleCreateResponse(res: ServerResponse, body: ResponsesAPIRequest, registry: ModelRegistry, store: ResponseStore | null, httpReq?: IncomingMessage, responseRetentionSec?: number, idleSweeper?: IdleSweeper | null, modelWorkCoordinator?: ModelWorkCoordinator,
92
+ /** Lazy-load hook. See the call site below and `ServerConfig.resolveModel`. */
93
+ resolveModel?: (name: string) => Promise<void>): Promise<void>;
92
94
  //# sourceMappingURL=responses.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/endpoints/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAc,WAAW,EAAc,aAAa,EAAwB,MAAM,gBAAgB,CAAC;AAM/G,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAStD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAcpD,OAAO,KAAK,EAMV,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAUrB;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,aAAa,CAAC;AAU5C;;;;;;;;;GASG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,aAAa,GAAG,OAAO,GAAG,YAAY,CAAC;AAmChF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iCAAiC,IAAI,MAAM,CAM1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAMnD;AA0BD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,2BAA2B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE5D;AAs9BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,WAAW,EAAE,EACvB,UAAU,GAAE,QAAQ,GAAG,WAAsB,GAC5C,MAAM,GAAG,IAAI,CAiHf;AA8WD,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,mBAAmB,EACzB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,aAAa,GAAG,IAAI,EAC3B,OAAO,CAAC,EAAE,eAAe,EACzB,oBAAoB,CAAC,EAAE,MAAM,EAC7B,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,EAChC,oBAAoB,CAAC,EAAE,oBAAoB,GAC1C,OAAO,CAAC,IAAI,CAAC,CAugEf"}
1
+ {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/endpoints/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAc,WAAW,EAAc,aAAa,EAAwB,MAAM,gBAAgB,CAAC;AAM/G,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAStD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAcpD,OAAO,KAAK,EAMV,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAUrB;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,aAAa,CAAC;AAU5C;;;;;;;;;GASG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,aAAa,GAAG,OAAO,GAAG,YAAY,CAAC;AAmChF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iCAAiC,IAAI,MAAM,CAM1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAMnD;AA0BD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,2BAA2B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE5D;AAs9BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,WAAW,EAAE,EACvB,UAAU,GAAE,QAAQ,GAAG,WAAsB,GAC5C,MAAM,GAAG,IAAI,CAiHf;AA+WD,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,mBAAmB,EACzB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,aAAa,GAAG,IAAI,EAC3B,OAAO,CAAC,EAAE,eAAe,EACzB,oBAAoB,CAAC,EAAE,MAAM,EAC7B,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,EAChC,oBAAoB,CAAC,EAAE,oBAAoB;AAC3C,+EAA+E;AAC/E,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAC7C,OAAO,CAAC,IAAI,CAAC,CAsiEf"}
@@ -1175,10 +1175,11 @@ async function runSessionNonStreaming(session, messages, newInputMessages, confi
1175
1175
  }
1176
1176
  // Hot path — session's KV cache is already warmed for this chain.
1177
1177
  // Single-message continuations whose role is `user` or `tool` take
1178
- // the cheap delta paths (`send` / `sendToolResult`). Any other single
1178
+ // the session paths (`send` / `sendToolResult`), which render the full
1179
+ // transcript and reuse KV on an exact token-prefix match. Any other single
1179
1180
  // role (`assistant`, `system`) is still accepted by `mapRequest` —
1180
1181
  // `reconstructMessagesFromChain` + `primeHistory` tolerate a tail of
1181
- // either — but the chat-session delta API has no entry point for
1182
+ // either — but the high-level chat-session API has no entry point for
1182
1183
  // them, so fall through to reset + cold re-prime against the fully
1183
1184
  // rebuilt history. Returning 500 here would regress the pre-session-
1184
1185
  // API full-history path, making valid continuation payloads fail
@@ -1412,7 +1413,9 @@ function readStoredModelIdentity(record) {
1412
1413
  // ---------------------------------------------------------------------------
1413
1414
  // Public handler
1414
1415
  // ---------------------------------------------------------------------------
1415
- export async function handleCreateResponse(res, body, registry, store, httpReq, responseRetentionSec, idleSweeper, modelWorkCoordinator) {
1416
+ export async function handleCreateResponse(res, body, registry, store, httpReq, responseRetentionSec, idleSweeper, modelWorkCoordinator,
1417
+ /** Lazy-load hook. See the call site below and `ServerConfig.resolveModel`. */
1418
+ resolveModel) {
1416
1419
  const handlerStartedAt = Date.now();
1417
1420
  // Validate required fields
1418
1421
  if (body == null || typeof body !== 'object') {
@@ -1472,6 +1475,36 @@ export async function handleCreateResponse(res, body, registry, store, httpReq,
1472
1475
  }
1473
1476
  }
1474
1477
  const effectiveRetentionSec = requestedRetentionSec ?? responseRetentionSec;
1478
+ // Lazy load, exactly as the Anthropic endpoints do. Nothing is resident at
1479
+ // boot — `createInferenceHost` only discovers — so without this the very
1480
+ // first `/v1/responses` 404s against a `/v1/models` list that advertises the
1481
+ // model, and a client id that exists only as an alias 404s forever.
1482
+ //
1483
+ // Placement is pinned on both sides. AFTER the pure validation above, so a
1484
+ // 400 cannot burn a 30 s load or evict the resident model. BEFORE
1485
+ // `registry.get` below, and therefore before the dispatch lease, which needs
1486
+ // a registered name.
1487
+ if (resolveModel) {
1488
+ // Errors serialize through the OpenAI envelope here. Letting them reach
1489
+ // the outer `createHandler` catch would be right for this endpoint by
1490
+ // accident and wrong for the Anthropic one — `messages.ts` has the mirror
1491
+ // of this note for the opposite reason.
1492
+ try {
1493
+ // Suspension OUTSIDE the writer lock, per `load-model.ts`: a load that
1494
+ // parks in `acquireWrite()` must already be covered, or the drain timer
1495
+ // armed by the previous request fires mid-materialization. `messages.ts`
1496
+ // nests these the other way round, which is safe only because
1497
+ // `withSuspendedDrains` is a counter rather than a mutex.
1498
+ const load = () => modelWorkCoordinator
1499
+ ? modelWorkCoordinator.withModelLoad(() => resolveModel(body.model), 'responses')
1500
+ : resolveModel(body.model);
1501
+ await (idleSweeper ? idleSweeper.withSuspendedDrains(load) : load());
1502
+ }
1503
+ catch (err) {
1504
+ sendInternalError(res, err instanceof Error ? err.message : 'Failed to resolve model');
1505
+ return;
1506
+ }
1507
+ }
1475
1508
  // Look up model
1476
1509
  const model = registry.get(body.model);
1477
1510
  if (!model) {
@@ -2126,7 +2159,7 @@ export async function handleCreateResponse(res, body, registry, store, httpReq,
2126
2159
  // removed on hit so overlapping requests against the same prior id
2127
2160
  // cannot race on the same single-flight ChatSession).
2128
2161
  //
2129
- // Hot-path eligibility gate: the chat-session delta API only
2162
+ // Hot-path eligibility gate: the high-level chat-session API only
2130
2163
  // serves a SINGLE `user` or `tool` continuation message — the
2131
2164
  // `send` / `sendToolResult` entry points cover exactly that
2132
2165
  // shape. A single `assistant` / `system` continuation cannot
@@ -2135,7 +2168,7 @@ export async function handleCreateResponse(res, body, registry, store, httpReq,
2135
2168
  // still VALID — it just routes through `runSession*`'s
2136
2169
  // `session.turns === 0` fall-through (`primeHistory` +
2137
2170
  // `startFromHistory*`) instead of the `send` / `sendToolResult`
2138
- // delta path. Crucially, a tier-1 HIT on this branch is still
2171
+ // session continuation path. Crucially, a tier-1 HIT on this branch is still
2139
2172
  // useful: `resetPreservingNativeCacheForWarmReuse(session)` keeps
2140
2173
  // the warm native KV cache, and the subsequent
2141
2174
  // `chat_session_start_sync` -> `verify_cache_prefix_direct`
package/dist/handler.d.ts CHANGED
@@ -1,6 +1,7 @@
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';
4
5
  import type { IdleSweeper } from './idle-sweeper.js';
5
6
  import { ModelWorkCoordinator } from './model-work-coordinator.js';
6
7
  import type { ModelRegistry } from './registry.js';
@@ -17,7 +18,14 @@ export interface PublicModelEntry {
17
18
  owned_by: string;
18
19
  }
19
20
  export interface HandlerOptions {
20
- /** 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
+ */
21
29
  cors?: boolean;
22
30
  /** Response store for previous_response_id support. */
23
31
  store?: ResponseStore | null;
@@ -55,6 +63,24 @@ export interface HandlerOptions {
55
63
  * returns this list instead of `registry.list()`.
56
64
  */
57
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;
58
84
  }
59
85
  export declare function createHandler(registry: ModelRegistry, options?: HandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
60
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,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD;;;;;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,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;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;CACvC;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,CA8C9D"}
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,31 +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';
4
+ import { createHealthReporter } from './health.js';
3
5
  import { ModelWorkCoordinator } from './model-work-coordinator.js';
4
- import { routeRequest } from './router.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']);
5
23
  export function createHandler(registry, options) {
6
- 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;
7
27
  const store = options?.store ?? null;
8
28
  const responseRetentionSec = options?.responseRetentionSec;
9
29
  const idleSweeper = options?.idleSweeper ?? null;
10
30
  const resolveModel = options?.resolveModel;
11
31
  const modelWorkCoordinator = options?.modelWorkCoordinator ?? (resolveModel ? new ModelWorkCoordinator() : undefined);
12
32
  const listModels = options?.listModels;
33
+ const health = options?.health ?? createHealthReporter({ registry, idleSweeper, modelWorkCoordinator: modelWorkCoordinator });
13
34
  return async (req, res) => {
14
- if (cors) {
15
- res.setHeader('Access-Control-Allow-Origin', '*');
16
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
17
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version');
18
- if (req.method === 'OPTIONS') {
19
- res.writeHead(204);
20
- res.end();
21
- return;
22
- }
23
- }
24
- // Returning the promise lets tests await the full lifecycle including
25
- // post-`res.end()` bookkeeping (e.g. `SessionRegistry.adopt`). `http.createServer`
26
- // 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.
27
40
  try {
28
- await routeRequest(req, res, registry, store, responseRetentionSec, idleSweeper, resolveModel, listModels, modelWorkCoordinator);
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 });
29
78
  }
30
79
  catch (err) {
31
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"}