@mlx-node/server 0.0.8 → 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 (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
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Single-resident lazy-load policy for an inference host.
3
+ *
4
+ * The host discovers every local model up-front but loads at most one into
5
+ * the `ModelRegistry` at a time. Switching models (e.g. via Claude Code's
6
+ * `/model` picker, or the desktop app's model menu) unregisters the previous
7
+ * instance, letting GC + native destructors reclaim memory, before loading
8
+ * the new one.
9
+ */
10
+ /**
11
+ * Build the `resolveModel` + `listModels` callbacks for the handler.
12
+ *
13
+ * `loadModelFn` is injected so tests can stub it without touching native code.
14
+ * The controller serializes every `resolveModel` invocation on a single
15
+ * promise chain so two concurrent requests for different-but-currently-
16
+ * unloaded models cannot race on the native compiled-path globals.
17
+ */
18
+ export function makeSwapController(discovered, registry, loadModelFn, defaultName) {
19
+ const byName = new Map();
20
+ for (const entry of discovered)
21
+ byName.set(entry.name, entry);
22
+ const ordered = [...discovered].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
23
+ // Which entry unknown names (haiku subagent dispatches, etc.) fall back
24
+ // to before anything is resident. Defaults to discovered[0], but the
25
+ // caller can pin it to the user's `--model` pick so the first haiku
26
+ // title-gen doesn't trigger a load of the alphabetically-first model
27
+ // followed by an immediate swap to the user's real choice.
28
+ const fallbackEntry = (defaultName != null ? byName.get(defaultName) : undefined) ?? discovered[0];
29
+ let resident = null;
30
+ // Names we registered as aliases to the current resident (e.g.
31
+ // Claude Code's hardcoded `claude-haiku-*` for subagent dispatches /
32
+ // title generation). Tracked so we can unregister them on `/model`
33
+ // swap — otherwise an alias's refcount would keep the old binding
34
+ // alive past the user's swap.
35
+ const aliases = new Set();
36
+ let currentOp = Promise.resolve();
37
+ async function resolveModel(name) {
38
+ // Fast path: already registered under this name (either as a real
39
+ // resident or as an alias we previously installed). Avoid chaining.
40
+ if (registry.get(name))
41
+ return;
42
+ const next = currentOp.then(async () => {
43
+ // Re-check under the serialized section — a prior waiter may have loaded it.
44
+ if (registry.get(name))
45
+ return;
46
+ // Pick the discovered entry to resolve against. If the requested
47
+ // name matches a discovered model, use it. Otherwise (unknown
48
+ // name — Claude Code's hardcoded small-fast-model, etc.) fall
49
+ // through to the current resident so subagent dispatches don't
50
+ // 404, loading discovered[0] on first boot if nothing is resident
51
+ // yet.
52
+ //
53
+ // CRITICAL: this must read `resident` at RUN time, not QUEUE time.
54
+ // If we capture it before chaining onto `currentOp`, a swap that
55
+ // ran ahead of us will leave us with a stale target — e.g. a haiku
56
+ // alias request that arrived during a `/model a → b` switch would
57
+ // capture `targetEntry = a`, then re-bind itself to `a` and undo
58
+ // the user's switch when its turn finally comes around.
59
+ const knownEntry = byName.get(name);
60
+ const targetEntry = knownEntry ?? (resident ? (byName.get(resident.name) ?? fallbackEntry) : fallbackEntry);
61
+ const isAlias = targetEntry.name !== name;
62
+ // Swap out any stale resident that isn't the target.
63
+ //
64
+ // We do NOT unregister the aliases here: in-flight messages.ts
65
+ // requests may be microtask-racing between "resolveModel returned"
66
+ // and "registry.get(body.model)" and dropping the alias in that
67
+ // window yields a spurious 404. Instead we carry the alias set
68
+ // across the swap and re-point them to the new resident below,
69
+ // so the name always resolves to *some* live instance.
70
+ const oldResident = resident;
71
+ const carriedAliases = new Set(aliases);
72
+ if (oldResident && oldResident.name !== targetEntry.name) {
73
+ // Drop our local alias bookkeeping AND the old resident's primary
74
+ // name binding, but leave the alias *names* in the registry pointed
75
+ // at the old model — they hold the only refcount preventing GC,
76
+ // and an in-flight `registry.get(alias)` must keep resolving to
77
+ // *some* live instance until the new model is in hand.
78
+ aliases.clear();
79
+ registry.unregister(oldResident.name);
80
+ resident = null;
81
+ }
82
+ // Ensure the target is resident. If the load throws, restore the
83
+ // pre-swap controller state so future swap attempts know about the
84
+ // aliases we just cleared — otherwise the alias *names* stay bound
85
+ // in the registry to the old model object forever (alias bindings
86
+ // hold their own refcount), but the controller forgets they exist
87
+ // and never repoints them, leaving alias-routed traffic permanently
88
+ // pinned to a stale model.
89
+ let instance = registry.get(targetEntry.name);
90
+ if (!instance) {
91
+ let loaded;
92
+ try {
93
+ loaded = await loadModelFn(targetEntry.path);
94
+ }
95
+ catch (err) {
96
+ // Recovery: re-populate the controller's alias set so the next
97
+ // resolveModel call still owns them. The alias→old-model bindings
98
+ // are still live in the registry (we never unregistered them), so
99
+ // we can recover the old model object via any surviving alias and
100
+ // re-bind the old resident's primary name.
101
+ //
102
+ // If `carriedAliases` is empty (no aliases ever existed) AND we
103
+ // unregistered `oldResident.name`, the binding's refcount may have
104
+ // hit zero and the model is gone. There's nothing the controller
105
+ // can do to recover in that case — the user will need to /model-
106
+ // pick again. This is acceptable: alias-less load failures are
107
+ // rare, and the user-facing symptom is "prior model gone, please
108
+ // re-pick", not silently-wrong responses.
109
+ for (const aliasName of carriedAliases)
110
+ aliases.add(aliasName);
111
+ if (oldResident) {
112
+ let oldInstance;
113
+ for (const aliasName of carriedAliases) {
114
+ const probe = registry.get(aliasName);
115
+ if (probe) {
116
+ oldInstance = probe;
117
+ break;
118
+ }
119
+ }
120
+ if (oldInstance) {
121
+ const oldEntry = byName.get(oldResident.name);
122
+ registry.register(oldResident.name, oldInstance, {
123
+ samplingDefaults: oldEntry?.preset.sampling,
124
+ maxOutputTokens: oldEntry?.preset.maxOutputTokens,
125
+ });
126
+ resident = oldResident;
127
+ }
128
+ }
129
+ throw err;
130
+ }
131
+ instance = loaded;
132
+ registry.register(targetEntry.name, instance, {
133
+ samplingDefaults: targetEntry.preset.sampling,
134
+ maxOutputTokens: targetEntry.preset.maxOutputTokens,
135
+ });
136
+ resident = { name: targetEntry.name };
137
+ }
138
+ else if (!resident) {
139
+ resident = { name: targetEntry.name };
140
+ }
141
+ // Re-point any aliases carried across the swap onto the new
142
+ // resident. `registry.register(sameName, differentModel)` drops
143
+ // the old binding's refcount and installs the new one atomically,
144
+ // so any concurrent `registry.get(alias)` either sees the old or
145
+ // new instance — never null.
146
+ for (const aliasName of carriedAliases) {
147
+ if (aliasName === targetEntry.name)
148
+ continue;
149
+ registry.register(aliasName, instance, {
150
+ samplingDefaults: targetEntry.preset.sampling,
151
+ maxOutputTokens: targetEntry.preset.maxOutputTokens,
152
+ });
153
+ aliases.add(aliasName);
154
+ }
155
+ // For unknown names, register an alias on the resident instance so
156
+ // the endpoint's `registry.get(name)` lookup succeeds.
157
+ if (isAlias) {
158
+ registry.register(name, instance, {
159
+ samplingDefaults: targetEntry.preset.sampling,
160
+ maxOutputTokens: targetEntry.preset.maxOutputTokens,
161
+ });
162
+ aliases.add(name);
163
+ }
164
+ });
165
+ currentOp = next.catch(() => undefined);
166
+ await next;
167
+ }
168
+ function listModels() {
169
+ const created = Math.floor(Date.now() / 1000);
170
+ return ordered.map((entry) => ({
171
+ id: entry.name,
172
+ object: 'model',
173
+ created,
174
+ owned_by: 'mlx-node',
175
+ }));
176
+ }
177
+ return { resolveModel, listModels };
178
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Pid-scoped temp roots for the host's paged-config overrides, plus the
3
+ * startup sweep that reclaims the ones a killed host left behind.
4
+ *
5
+ * `PagedConfigOverrideManager` clones a checkpoint directory (config.json
6
+ * rewritten, everything else symlinked) into a temp root and removes that root
7
+ * in `cleanup()`. `cleanup()` runs on a normal shutdown — but the host's
8
+ * headline deployment is an Electron `utilityProcess`, and a `utilityProcess`
9
+ * can be SIGKILLed (app force-quit, OOM killer, `kill -9`). SIGKILL runs no
10
+ * handler, so without a sweep EVERY hard kill leaks a root. The clones are
11
+ * symlink farms rather than copies, so the leak is inodes and directory
12
+ * entries rather than model-sized bytes — but the roots accumulate forever and
13
+ * a partially-written clone can hold a real config.json.
14
+ *
15
+ * The reclaim strategy is "name the owner in the directory name": the manager
16
+ * gets a `tempDirPrefix` of `mlx-inference-host-<pid>-`, `mkdtemp` appends its
17
+ * own random suffix, and {@link sweepOrphanHostTempRoots} parses the pid back
18
+ * out and removes any root whose owner is gone.
19
+ *
20
+ * Known limitation: pid reuse. A long-dead host's root whose pid has since
21
+ * been recycled by an unrelated process is SPARED (never wrongly deleted), so
22
+ * the failure mode is a leaked directory, not data loss.
23
+ */
24
+ /** Shared stem. A directory is host-owned iff its name starts with this. */
25
+ export declare const HOST_TEMP_DIR_STEM = "mlx-inference-host-";
26
+ /**
27
+ * `tempDirPrefix` to hand `PagedConfigOverrideManager` so the root it creates
28
+ * carries its owner's pid.
29
+ */
30
+ export declare function hostTempDirPrefix(pid?: number): string;
31
+ /**
32
+ * Does a process with this pid exist?
33
+ *
34
+ * `process.kill(pid, 0)` sends no signal; it only performs the permission +
35
+ * existence check. `ESRCH` is the sole "gone" answer — `EPERM` means the
36
+ * process exists but belongs to another user, which must count as ALIVE so a
37
+ * multi-user box never has one user's sweep delete another user's live root.
38
+ */
39
+ export declare function isProcessAlive(pid: number): boolean;
40
+ export interface SweepOrphanTempRootsOptions {
41
+ /** Directory to scan. Defaults to the OS temp dir. */
42
+ root?: string;
43
+ /** Our own pid — never swept, however the liveness probe answers. */
44
+ selfPid?: number;
45
+ /** Injectable liveness probe. Defaults to {@link isProcessAlive}. */
46
+ isAlive?: (pid: number) => boolean;
47
+ }
48
+ /**
49
+ * Remove every host temp root whose owning pid is no longer alive.
50
+ *
51
+ * Best effort by contract: a scan or unlink failure (permissions, a root a
52
+ * concurrently-exiting host is removing under us) is swallowed, because a
53
+ * housekeeping step must never be the reason a host refuses to start.
54
+ * Returns the absolute paths actually removed.
55
+ */
56
+ export declare function sweepOrphanHostTempRoots(opts?: SweepOrphanTempRootsOptions): Promise<string[]>;
57
+ //# sourceMappingURL=temp-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temp-root.d.ts","sourceRoot":"","sources":["../../src/host/temp-root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AAKxD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAoB,GAAG,MAAM,CAEnE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWnD;AAED,MAAM,WAAW,2BAA2B;IAC1C,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,GAAE,2BAAgC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA6BxG"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Pid-scoped temp roots for the host's paged-config overrides, plus the
3
+ * startup sweep that reclaims the ones a killed host left behind.
4
+ *
5
+ * `PagedConfigOverrideManager` clones a checkpoint directory (config.json
6
+ * rewritten, everything else symlinked) into a temp root and removes that root
7
+ * in `cleanup()`. `cleanup()` runs on a normal shutdown — but the host's
8
+ * headline deployment is an Electron `utilityProcess`, and a `utilityProcess`
9
+ * can be SIGKILLed (app force-quit, OOM killer, `kill -9`). SIGKILL runs no
10
+ * handler, so without a sweep EVERY hard kill leaks a root. The clones are
11
+ * symlink farms rather than copies, so the leak is inodes and directory
12
+ * entries rather than model-sized bytes — but the roots accumulate forever and
13
+ * a partially-written clone can hold a real config.json.
14
+ *
15
+ * The reclaim strategy is "name the owner in the directory name": the manager
16
+ * gets a `tempDirPrefix` of `mlx-inference-host-<pid>-`, `mkdtemp` appends its
17
+ * own random suffix, and {@link sweepOrphanHostTempRoots} parses the pid back
18
+ * out and removes any root whose owner is gone.
19
+ *
20
+ * Known limitation: pid reuse. A long-dead host's root whose pid has since
21
+ * been recycled by an unrelated process is SPARED (never wrongly deleted), so
22
+ * the failure mode is a leaked directory, not data loss.
23
+ */
24
+ import { readdir, rm } from 'node:fs/promises';
25
+ import { tmpdir } from 'node:os';
26
+ import { join } from 'node:path';
27
+ /** Shared stem. A directory is host-owned iff its name starts with this. */
28
+ export const HOST_TEMP_DIR_STEM = 'mlx-inference-host-';
29
+ /** Matches `mlx-inference-host-<pid>-<mkdtemp suffix>`. */
30
+ const HOST_TEMP_DIR_RE = /^mlx-inference-host-(\d+)-/;
31
+ /**
32
+ * `tempDirPrefix` to hand `PagedConfigOverrideManager` so the root it creates
33
+ * carries its owner's pid.
34
+ */
35
+ export function hostTempDirPrefix(pid = process.pid) {
36
+ return `${HOST_TEMP_DIR_STEM}${pid}-`;
37
+ }
38
+ /**
39
+ * Does a process with this pid exist?
40
+ *
41
+ * `process.kill(pid, 0)` sends no signal; it only performs the permission +
42
+ * existence check. `ESRCH` is the sole "gone" answer — `EPERM` means the
43
+ * process exists but belongs to another user, which must count as ALIVE so a
44
+ * multi-user box never has one user's sweep delete another user's live root.
45
+ */
46
+ export function isProcessAlive(pid) {
47
+ // pid 0 addresses the caller's whole process group on POSIX and pid < 0 a
48
+ // group by id; neither is ever a real owner, and signalling them would be
49
+ // actively dangerous. Treat as alive so they are never swept.
50
+ if (!Number.isInteger(pid) || pid <= 0)
51
+ return true;
52
+ try {
53
+ process.kill(pid, 0);
54
+ return true;
55
+ }
56
+ catch (err) {
57
+ return err.code !== 'ESRCH';
58
+ }
59
+ }
60
+ /**
61
+ * Remove every host temp root whose owning pid is no longer alive.
62
+ *
63
+ * Best effort by contract: a scan or unlink failure (permissions, a root a
64
+ * concurrently-exiting host is removing under us) is swallowed, because a
65
+ * housekeeping step must never be the reason a host refuses to start.
66
+ * Returns the absolute paths actually removed.
67
+ */
68
+ export async function sweepOrphanHostTempRoots(opts = {}) {
69
+ const root = opts.root ?? tmpdir();
70
+ const selfPid = opts.selfPid ?? process.pid;
71
+ const isAlive = opts.isAlive ?? isProcessAlive;
72
+ let entries;
73
+ try {
74
+ entries = await readdir(root);
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ const removed = [];
80
+ for (const name of entries) {
81
+ const match = HOST_TEMP_DIR_RE.exec(name);
82
+ if (match === null)
83
+ continue;
84
+ const pid = Number.parseInt(match[1], 10);
85
+ if (pid === selfPid)
86
+ continue;
87
+ if (isAlive(pid))
88
+ continue;
89
+ const full = join(root, name);
90
+ try {
91
+ await rm(full, { recursive: true, force: true });
92
+ removed.push(full);
93
+ }
94
+ catch {
95
+ /* another sweeper won the race, or we lack permission; leave it */
96
+ }
97
+ }
98
+ return removed;
99
+ }
package/dist/index.d.ts CHANGED
@@ -5,7 +5,16 @@
5
5
  * `GET /v1/models`, in both streaming (SSE) and non-streaming modes.
6
6
  */
7
7
  export { createServer } from './server.js';
8
- export type { ServerConfig, ServerInstance } from './server.js';
8
+ export type { CloseOptions, CloseResult, ServerConfig, ServerInstance } from './server.js';
9
+ /**
10
+ * Readiness reporting. `deriveHealthStatus` is pure — a supervisor can reuse
11
+ * it to classify a `ServerHealth` body fetched over HTTP.
12
+ */
13
+ export { createHealthReporter, deriveHealthStatus, toMinimalHealth } from './health.js';
14
+ export type { HealthReporterDeps, HealthStatusInputs, ModelLoadRecord, ServerHealth, ServerHealthMinimal, ServerHealthStatus, } from './health.js';
15
+ export { ModelWorkCoordinator } from './model-work-coordinator.js';
16
+ export type { ModelLoadOutcome } from './model-work-coordinator.js';
17
+ export type { GuardedLoadDeps, LoadModelOptions } from './load-model.js';
9
18
  /**
10
19
  * Internal helpers re-exported for unit testing only. Not part of the
11
20
  * supported public API — names may change without notice.
@@ -25,5 +34,5 @@ export type { LaunchPreset } from './presets.js';
25
34
  export type { PublicModelEntry } from './handler.js';
26
35
  export type { ResponsesAPIRequest, ResponseObject, ResponseUsage, ResponseError, InputItem, InputMessage, InputFunctionCall, InputFunctionCallOutput, OutputItem, MessageOutputItem, ReasoningOutputItem, FunctionCallOutputItem, OutputTextPart, SummaryTextPart, ResponsesToolDefinition, ContentPart, InputTextPart, StreamEvent, } from './types.js';
27
36
  export type { AnthropicCountTokensRequest, AnthropicCountTokensResponse, AnthropicMessagesRequest, AnthropicMessagesResponse, AnthropicMessage, AnthropicContentBlock, AnthropicTextContentBlock, AnthropicImageContentBlock, AnthropicToolResultContentBlock, AnthropicToolUseContentBlock, AnthropicThinkingContentBlock, AnthropicToolDefinition, AnthropicToolChoice, AnthropicResponseContent, AnthropicResponseTextBlock, AnthropicResponseThinkingBlock, AnthropicResponseToolUseBlock, AnthropicUsage, AnthropicStreamEvent, AnthropicMessageStartEvent, AnthropicContentBlockStartEvent, AnthropicContentBlockDeltaEvent, AnthropicContentBlockStopEvent, AnthropicMessageDeltaEvent, AnthropicMessageStopEvent, AnthropicDelta, AnthropicTextDelta, AnthropicThinkingDelta, AnthropicInputJsonDelta, SystemBlock, } from './types-anthropic.js';
28
- export { writeSSEEvent, beginSSE, endSSE } from './streaming.js';
37
+ export { writeSSEEvent, beginSSE, endSSE, activeSSEStreamCount } from './streaming.js';
29
38
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE;;;GAGG;AACH,OAAO,EAAE,eAAe,IAAI,iBAAiB,EAAE,mBAAmB,IAAI,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACjH,OAAO,EACL,iBAAiB,IAAI,mBAAmB,EACxC,sBAAsB,IAAI,wBAAwB,EAClD,2BAA2B,IAAI,6BAA6B,GAC7D,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEtG,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAS1D,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACxH,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,2BAA2B,EAC3B,4BAA4B,EAC5B,wBAAwB,EACxB,yBAAyB,EACzB,gBAAgB,EAChB,qBAAqB,EACrB,yBAAyB,EACzB,0BAA0B,EAC1B,+BAA+B,EAC/B,4BAA4B,EAC5B,6BAA6B,EAC7B,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,6BAA6B,EAC7B,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,EAC9B,0BAA0B,EAC1B,yBAAyB,EACzB,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,WAAW,GACZ,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3F;;;GAGG;AACH,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACxF,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEzE;;;GAGG;AACH,OAAO,EAAE,eAAe,IAAI,iBAAiB,EAAE,mBAAmB,IAAI,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACjH,OAAO,EACL,iBAAiB,IAAI,mBAAmB,EACxC,sBAAsB,IAAI,wBAAwB,EAClD,2BAA2B,IAAI,6BAA6B,GAC7D,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEtG,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAS1D,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACxH,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,2BAA2B,EAC3B,4BAA4B,EAC5B,wBAAwB,EACxB,yBAAyB,EACzB,gBAAgB,EAChB,qBAAqB,EACrB,yBAAyB,EACzB,0BAA0B,EAC1B,+BAA+B,EAC/B,4BAA4B,EAC5B,6BAA6B,EAC7B,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,6BAA6B,EAC7B,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,EAC9B,0BAA0B,EAC1B,yBAAyB,EACzB,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,WAAW,GACZ,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,12 @@
5
5
  * `GET /v1/models`, in both streaming (SSE) and non-streaming modes.
6
6
  */
7
7
  export { createServer } from './server.js';
8
+ /**
9
+ * Readiness reporting. `deriveHealthStatus` is pure — a supervisor can reuse
10
+ * it to classify a `ServerHealth` body fetched over HTTP.
11
+ */
12
+ export { createHealthReporter, deriveHealthStatus, toMinimalHealth } from './health.js';
13
+ export { ModelWorkCoordinator } from './model-work-coordinator.js';
8
14
  /**
9
15
  * Internal helpers re-exported for unit testing only. Not part of the
10
16
  * supported public API — names may change without notice.
@@ -23,4 +29,4 @@ export { resolveServerTuningForUsage } from './timing.js';
23
29
  // import it from the deep path
24
30
  // `packages/server/src/session-registry.js` instead.
25
31
  export { QWEN_SAMPLING_DEFAULTS, GEMMA4_SAMPLING_DEFAULTS, LFM2_SAMPLING_DEFAULTS, LAUNCH_PRESETS } from './presets.js';
26
- export { writeSSEEvent, beginSSE, endSSE } from './streaming.js';
32
+ export { writeSSEEvent, beginSSE, endSSE, activeSSEStreamCount } from './streaming.js';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Safe out-of-band model load.
3
+ *
4
+ * A supervisor (or a `/model`-picker UI) needs to swap the resident model on a
5
+ * server that is already serving. Doing that naively races the Metal
6
+ * allocator — a corruption/abort bug, not a latency bug — so the bracketing
7
+ * order below is load-bearing:
8
+ *
9
+ * ```text
10
+ * idleSweeper.withSuspendedDrains( ← OUTSIDE
11
+ * modelWorkCoordinator.withModelLoad( ← INSIDE
12
+ * load() ; registry.register(...)
13
+ * , label))
14
+ * ```
15
+ *
16
+ * Why the suspension must be OUTSIDE the writer lock: `endRequest()` arms a
17
+ * drain timer at `t + idleClearCacheMs` whose callback runs
18
+ * `__internal__.clearCache()`, walking the process-wide MLX free pool. A load
19
+ * that arrives while another load holds the writer slot parks inside
20
+ * `acquireWrite()` — and if the suspension were taken only after the lock was
21
+ * won, that armed timer would still be live during the wait AND during the
22
+ * hand-off, firing mid-materialization. Suspending first covers the wait, the
23
+ * lock hand-off, `load()`, and `register()` as one interval.
24
+ *
25
+ * Why the writer lock must be INSIDE: `withModelLoad` takes the exclusive
26
+ * writer slot, which excludes inference readers for exactly as long as
27
+ * weights are being materialized — no longer. Hoisting it outside the
28
+ * suspension would gain nothing and would hold readers off during the drain
29
+ * bookkeeping too.
30
+ *
31
+ * See `ServerInstance.withSuspendedDrains` and the `__internal__.clearCache`
32
+ * rustdoc in `packages/core/index.d.cts` for the underlying contracts.
33
+ */
34
+ import type { ChatConfig } from '@mlx-node/core';
35
+ import type { IdleSweeper } from './idle-sweeper.js';
36
+ import type { ModelWorkCoordinator } from './model-work-coordinator.js';
37
+ import type { ModelRegistry, ServableModel } from './registry.js';
38
+ export interface GuardedLoadDeps {
39
+ /** Only `withSuspendedDrains` is used; typed narrowly so tests can double it. */
40
+ idleSweeper: Pick<IdleSweeper, 'withSuspendedDrains'>;
41
+ modelWorkCoordinator: ModelWorkCoordinator;
42
+ registry: ModelRegistry;
43
+ }
44
+ export interface LoadModelOptions {
45
+ /** Primary registration name. Also used as the `/health` load label. */
46
+ name: string;
47
+ /** Materializes the model. Invoked exactly once, inside both brackets. */
48
+ load: () => Promise<ServableModel>;
49
+ /**
50
+ * Extra names bound to the SAME instance. Aliases share one
51
+ * `SessionRegistry`, preserving the single-warm invariant across names.
52
+ * An alias equal to `name` is ignored rather than re-registered.
53
+ */
54
+ aliases?: string[];
55
+ /** Per-model sampling defaults; forwarded to every alias too. */
56
+ samplingDefaults?: ChatConfig;
57
+ /** Per-model output-token clamp; forwarded to every alias too. */
58
+ maxOutputTokens?: number;
59
+ }
60
+ /**
61
+ * Load a model and register it under `name` (plus any aliases) with drains
62
+ * suspended and inference excluded for the whole materialization window.
63
+ *
64
+ * Rejects with the underlying error if `load()` (or `register`) throws;
65
+ * both brackets unwind via their own `finally`, so neither the suspend
66
+ * counter nor the writer lock can leak.
67
+ */
68
+ export declare function runGuardedModelLoad(deps: GuardedLoadDeps, opts: LoadModelOptions): Promise<void>;
69
+ //# sourceMappingURL=load-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-model.d.ts","sourceRoot":"","sources":["../src/load-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAmB,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnF,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,qBAAqB,CAAC,CAAC;IACtD,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,UAAU,CAAC;IAC9B,kEAAkE;IAClE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBtG"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Safe out-of-band model load.
3
+ *
4
+ * A supervisor (or a `/model`-picker UI) needs to swap the resident model on a
5
+ * server that is already serving. Doing that naively races the Metal
6
+ * allocator — a corruption/abort bug, not a latency bug — so the bracketing
7
+ * order below is load-bearing:
8
+ *
9
+ * ```text
10
+ * idleSweeper.withSuspendedDrains( ← OUTSIDE
11
+ * modelWorkCoordinator.withModelLoad( ← INSIDE
12
+ * load() ; registry.register(...)
13
+ * , label))
14
+ * ```
15
+ *
16
+ * Why the suspension must be OUTSIDE the writer lock: `endRequest()` arms a
17
+ * drain timer at `t + idleClearCacheMs` whose callback runs
18
+ * `__internal__.clearCache()`, walking the process-wide MLX free pool. A load
19
+ * that arrives while another load holds the writer slot parks inside
20
+ * `acquireWrite()` — and if the suspension were taken only after the lock was
21
+ * won, that armed timer would still be live during the wait AND during the
22
+ * hand-off, firing mid-materialization. Suspending first covers the wait, the
23
+ * lock hand-off, `load()`, and `register()` as one interval.
24
+ *
25
+ * Why the writer lock must be INSIDE: `withModelLoad` takes the exclusive
26
+ * writer slot, which excludes inference readers for exactly as long as
27
+ * weights are being materialized — no longer. Hoisting it outside the
28
+ * suspension would gain nothing and would hold readers off during the drain
29
+ * bookkeeping too.
30
+ *
31
+ * See `ServerInstance.withSuspendedDrains` and the `__internal__.clearCache`
32
+ * rustdoc in `packages/core/index.d.cts` for the underlying contracts.
33
+ */
34
+ /**
35
+ * Load a model and register it under `name` (plus any aliases) with drains
36
+ * suspended and inference excluded for the whole materialization window.
37
+ *
38
+ * Rejects with the underlying error if `load()` (or `register`) throws;
39
+ * both brackets unwind via their own `finally`, so neither the suspend
40
+ * counter nor the writer lock can leak.
41
+ */
42
+ export async function runGuardedModelLoad(deps, opts) {
43
+ // Build the register options once, omitting keys the caller never set.
44
+ // `ModelRegistry.register` uses `'samplingDefaults' in opts` to decide
45
+ // whether to OVERWRITE an existing binding's defaults, so passing an
46
+ // explicit `undefined` would silently clear them on a re-register.
47
+ const registerOpts = {};
48
+ if (opts.samplingDefaults !== undefined)
49
+ registerOpts.samplingDefaults = opts.samplingDefaults;
50
+ if (opts.maxOutputTokens !== undefined)
51
+ registerOpts.maxOutputTokens = opts.maxOutputTokens;
52
+ await deps.idleSweeper.withSuspendedDrains(async () => {
53
+ await deps.modelWorkCoordinator.withModelLoad(async () => {
54
+ const instance = await opts.load();
55
+ deps.registry.register(opts.name, instance, registerOpts);
56
+ for (const alias of opts.aliases ?? []) {
57
+ if (alias === opts.name)
58
+ continue;
59
+ deps.registry.register(alias, instance, registerOpts);
60
+ }
61
+ }, opts.name);
62
+ });
63
+ }
@@ -1,3 +1,4 @@
1
+ import type { ModelLoadRecord } from './health.js';
1
2
  /**
2
3
  * Result of a `withModelLoad` call that surfaces who actually drove the load
3
4
  * vs. who merely parked behind one that was already in flight. Callers use
@@ -41,11 +42,35 @@ export interface ModelLoadOutcome<T> {
41
42
  */
42
43
  export declare class ModelWorkCoordinator {
43
44
  private activeReaders;
44
- private writerActive;
45
- private waitingWriters;
45
+ private writerHeld;
46
+ private queuedWriters;
46
47
  private readonly readerWaiters;
47
48
  private readonly writerWaiters;
48
- withModelLoad<T>(fn: () => Promise<T> | T): Promise<T>;
49
+ /**
50
+ * Most recent settled load bracket. Retained here because the coordinator
51
+ * is the ONE place that brackets every load: a `resolveModel` failure in
52
+ * `/v1/messages` becomes a 500 and is otherwise dropped on the floor, so a
53
+ * supervisor polling `/health` afterwards had no way to learn what went
54
+ * wrong. See {@link ModelLoadRecord} for the "successful no-op overwrites
55
+ * an earlier failure" caveat.
56
+ */
57
+ private lastLoadRecord;
58
+ /** Read-only: `true` while a load holds the exclusive writer slot. */
59
+ get writerActive(): boolean;
60
+ /** Read-only: loads parked in `acquireWrite()` waiting for the slot. */
61
+ get waitingWriters(): number;
62
+ /** Read-only: outcome of the most recent settled load bracket, or `null`. */
63
+ get lastLoad(): ModelLoadRecord | null;
64
+ /**
65
+ * Record a settled bracket. Called from the `finally` of both load
66
+ * wrappers so a throw is captured just as reliably as a success.
67
+ */
68
+ private recordLoad;
69
+ /**
70
+ * @param label Optional identifier (normally the model name) stamped into
71
+ * {@link lastLoad} so `/health` can name what was being loaded.
72
+ */
73
+ withModelLoad<T>(fn: () => Promise<T> | T, label?: string): Promise<T>;
49
74
  /**
50
75
  * Like {@link withModelLoad} but reports whether THIS caller owned the
51
76
  * load (acquired the writer lock with no contention) or merely waited
@@ -59,7 +84,7 @@ export declare class ModelWorkCoordinator {
59
84
  * 60-second cold-load does not look like 60 seconds of own work for
60
85
  * every concurrent request.
61
86
  */
62
- withModelLoadInstrumented<T>(fn: () => Promise<T> | T): Promise<ModelLoadOutcome<T>>;
87
+ withModelLoadInstrumented<T>(fn: () => Promise<T> | T, label?: string): Promise<ModelLoadOutcome<T>>;
63
88
  withInference<T>(fn: () => Promise<T> | T): Promise<T>;
64
89
  private acquireRead;
65
90
  private acquireWrite;
@@ -1 +1 @@
1
- {"version":3,"file":"model-work-coordinator.d.ts","sourceRoot":"","sources":["../src/model-work-coordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC;IACV,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IAEjD,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO3D;IAED;;;;;;;;;;;;OAYG;IACG,yBAAyB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAoBzF;IAEK,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO3D;IAED,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,KAAK;CAWd"}
1
+ {"version":3,"file":"model-work-coordinator.d.ts","sourceRoot":"","sources":["../src/model-work-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAqBnD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC;IACV,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc,CAAgC;IAEtD,sEAAsE;IACtE,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,wEAAwE;IACxE,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,6EAA6E;IAC7E,IAAI,QAAQ,IAAI,eAAe,GAAG,IAAI,CAErC;IAED;;;OAGG;IACH,OAAO,CAAC,UAAU;IAUlB;;;OAGG;IACG,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAmB3E;IAED;;;;;;;;;;;;OAYG;IACG,yBAAyB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CA2BzG;IAEK,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO3D;IAED,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,KAAK;CAWd"}