@mlx-node/server 0.0.0 → 0.0.8
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/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 +68 -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 +20 -7
- package/dist/endpoints/responses.d.ts.map +1 -1
- package/dist/endpoints/responses.js +572 -82
- 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 +42 -0
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +6 -1
- 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 +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -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 +70 -0
- package/dist/model-work-coordinator.d.ts.map +1 -0
- package/dist/model-work-coordinator.js +120 -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 +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +34 -4
- package/dist/server.d.ts +76 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +48 -1
- package/dist/session-registry.d.ts +272 -18
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +509 -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/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 +5 -5
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle cache-pool sweeper.
|
|
3
|
+
*
|
|
4
|
+
* The MLX Metal allocator holds a process-wide free pool. Without an
|
|
5
|
+
* explicit drain the pool can sit near the wired ceiling (tens of GB on
|
|
6
|
+
* an M3 Max) for the entire server uptime. `@mlx-node/core`'s decode
|
|
7
|
+
* loop already calls `mlx_clear_cache()` every 256 steps, but that
|
|
8
|
+
* cadence only fires while a generation is in progress — idle periods
|
|
9
|
+
* between HTTP requests never get a drain.
|
|
10
|
+
*
|
|
11
|
+
* An earlier iteration put a `ClearCacheOnDrop` RAII guard around every
|
|
12
|
+
* session command so the pool was flushed after every turn. That is
|
|
13
|
+
* wrong on a multi-model server: the free pool is shared across model
|
|
14
|
+
* instances, so flushing after a request on model A discards blocks
|
|
15
|
+
* that model B's next turn is about to reuse.
|
|
16
|
+
*
|
|
17
|
+
* The round-1 replacement was a debounced `touch()`-based sweeper:
|
|
18
|
+
* every request start / end reset a 30s timer that fired `clearCache()`
|
|
19
|
+
* when the timer expired. That design had a correctness bug flagged in
|
|
20
|
+
* round-2 review: a long-running request (streaming decode of 60+
|
|
21
|
+
* seconds) had its 30s timer armed at ARRIVAL, and the timer fired
|
|
22
|
+
* mid-request at t=30s — exactly when the generation thread still
|
|
23
|
+
* needed those blocks. Worse, `clearCache()` routes through
|
|
24
|
+
* `mlx_synchronize()` WITHOUT a stream argument, which only drains the
|
|
25
|
+
* default stream (see `crates/mlx-sys/mlx/mlx/scheduler.cpp`), but
|
|
26
|
+
* decode runs on custom streams — so the drain could race with live
|
|
27
|
+
* command buffers, risking buffer use-after-free.
|
|
28
|
+
*
|
|
29
|
+
* This version fixes both by tracking `inFlight` explicitly. The timer
|
|
30
|
+
* is ONLY armed when the counter returns to zero, and is cancelled the
|
|
31
|
+
* instant any new request arrives — mid-request drains are impossible
|
|
32
|
+
* by construction.
|
|
33
|
+
*
|
|
34
|
+
* # Scope: inference endpoints only
|
|
35
|
+
*
|
|
36
|
+
* The counter is bumped by `/v1/responses` and `/v1/messages` ONLY.
|
|
37
|
+
* Non-inference traffic — `/v1/models`, `/v1/health`, CORS `OPTIONS`
|
|
38
|
+
* preflights, 404s for unknown routes — deliberately does NOT touch
|
|
39
|
+
* the sweeper. Counting those would let a `GET /v1/models` on every
|
|
40
|
+
* client startup, a CORS preflight from a browser, or a cron health
|
|
41
|
+
* probe keep the allocator pinned forever.
|
|
42
|
+
*
|
|
43
|
+
* # Drain is post-request only
|
|
44
|
+
*
|
|
45
|
+
* The only drain path is post-request: `endRequest()` decrements the
|
|
46
|
+
* in-flight counter and, on the 1 -> 0 transition, arms the timer. A
|
|
47
|
+
* cold-idle server that loaded models but never served a request
|
|
48
|
+
* never drains. That is deliberate:
|
|
49
|
+
*
|
|
50
|
+
* - Earlier iterations armed a cold-start drain from either
|
|
51
|
+
* `createServer()` (regressed on slow model loads > `idleClearCacheMs`,
|
|
52
|
+
* firing mid-load) or from `ModelRegistry.register()` (round-7 review
|
|
53
|
+
* surfaced that it still raced sequential multi-model loads — the
|
|
54
|
+
* timer armed by `register(A)` could fire while `await load(B)` was
|
|
55
|
+
* still resolving). Loader-bracketing would require API changes on
|
|
56
|
+
* the user-facing load path, so we remove the cold-start path
|
|
57
|
+
* entirely instead.
|
|
58
|
+
* - Load-time allocator growth is bounded (a few GB of scratch), not
|
|
59
|
+
* the tens-of-GB problem the post-request drain targets. macOS
|
|
60
|
+
* handles truly-idle memory pressure via compression / swap.
|
|
61
|
+
* - Python `mlx-lm` has no cold-start drain either, so this matches
|
|
62
|
+
* that baseline for servers that never receive a request.
|
|
63
|
+
*
|
|
64
|
+
* # Hot-load bracketing: `withSuspendedDrains`
|
|
65
|
+
*
|
|
66
|
+
* Hot-load flows — a `Model::load()` invoked AFTER the server has
|
|
67
|
+
* already served at least one request — race the post-request drain
|
|
68
|
+
* timer: the t+delayMs timer armed by the previous `endRequest()` can
|
|
69
|
+
* fire MID-LOAD while weight materialization is still allocating
|
|
70
|
+
* through the Metal free pool. The canonical fix is to bracket the
|
|
71
|
+
* load with `withSuspendedDrains(fn)`:
|
|
72
|
+
*
|
|
73
|
+
* ```ts
|
|
74
|
+
* await server.withSuspendedDrains(async () => {
|
|
75
|
+
* const model = await Qwen35MoeModel.load(modelPath);
|
|
76
|
+
* server.registry.register('qwen', model);
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* `withSuspendedDrains` handles try/finally bracketing itself, so a
|
|
81
|
+
* thrown load never leaks the internal suspend counter — a footgun the
|
|
82
|
+
* earlier raw `suspendDrains()` / `resumeDrains()` pair exposed
|
|
83
|
+
* (round-9 MEDIUM). The low-level `suspendDrains()` entry point
|
|
84
|
+
* remains available for callers that genuinely need manual control;
|
|
85
|
+
* its returned disposer is token-scoped and idempotent.
|
|
86
|
+
*
|
|
87
|
+
* `withSuspendedDrains` is also thenable-safe: non-Promise thenables
|
|
88
|
+
* (plain objects or even callables that expose a `.then` method) are
|
|
89
|
+
* normalized via `Promise.resolve()` so callers always get a real
|
|
90
|
+
* Promise<T> back and the suspend-release fires on settle. A throwing
|
|
91
|
+
* `.then` getter is treated as non-thenable (the getter access is
|
|
92
|
+
* guarded); the value is returned directly and the suspend releases
|
|
93
|
+
* synchronously — we deliberately do NOT propagate a getter throw, on
|
|
94
|
+
* the theory that leaking the suspend is a far worse failure mode than
|
|
95
|
+
* losing a pathological value (round-10 MEDIUM).
|
|
96
|
+
*
|
|
97
|
+
* # Tuning
|
|
98
|
+
*
|
|
99
|
+
* - The default (30_000 ms) balances "give models a chance to reuse
|
|
100
|
+
* hot blocks across back-to-back requests" against "don't hold
|
|
101
|
+
* tens of GB hostage while the process is truly idle".
|
|
102
|
+
* - `idleClearCacheMs: 0` disables the sweeper entirely — useful for
|
|
103
|
+
* benchmarks or single-model workloads where the only memory
|
|
104
|
+
* pressure is the decode-loop cadence already in place.
|
|
105
|
+
* `withSuspendedDrains(fn)` on the disabled sweeper is a pass-through
|
|
106
|
+
* so call sites can unconditionally bracket.
|
|
107
|
+
* - `MLX_IDLE_CLEAR_CACHE_MS` env var overrides the server's
|
|
108
|
+
* constructor value; explicit constructor value wins over env.
|
|
109
|
+
*
|
|
110
|
+
* Drain cost is a single `mlx_synchronize` + `mlx_clear_cache` —
|
|
111
|
+
* constant time relative to generation length.
|
|
112
|
+
*/
|
|
113
|
+
// `clearCache` lives under the `__internal__` NAPI namespace — it is
|
|
114
|
+
// deliberately NOT re-exported on the root `@mlx-node/core` object to
|
|
115
|
+
// keep the process-wide, custom-stream drain out of the public
|
|
116
|
+
// surface. User code that deep-imports from `@mlx-node/core` must
|
|
117
|
+
// acknowledge the namespace (`core.__internal__.clearCache`) and
|
|
118
|
+
// read the `@internal` caveat there. See
|
|
119
|
+
// `crates/mlx-core/src/cache_limit.rs`.
|
|
120
|
+
//
|
|
121
|
+
// We import the module namespace (NOT a destructured `__internal__`)
|
|
122
|
+
// so that a stale `.node` binary missing the `__internal__` namespace
|
|
123
|
+
// does NOT hard-fail the import of `@mlx-node/server` itself. The
|
|
124
|
+
// `clearCache` symbol is resolved LAZILY inside `createIdleSweeper`
|
|
125
|
+
// (see round-5 Finding A): if the namespace / function is absent on
|
|
126
|
+
// the loaded binding we warn once and fall back to a no-op drain,
|
|
127
|
+
// keeping the server runnable on partial upgrades / downgrades. The
|
|
128
|
+
// expected resolution path is `core.__internal__.clearCache` — the
|
|
129
|
+
// sweeper never touches the root namespace so there is no ambiguity
|
|
130
|
+
// with the deliberately-omitted root-level `clearCache` export.
|
|
131
|
+
import * as core from '@mlx-node/core';
|
|
132
|
+
/** Default idle window before draining the allocator's free pool (ms). */
|
|
133
|
+
export const DEFAULT_IDLE_CLEAR_CACHE_MS = 30_000;
|
|
134
|
+
/**
|
|
135
|
+
* Promise/A+ thenable probe that is robust against:
|
|
136
|
+
*
|
|
137
|
+
* - Function-typed thenables (awkward but legal — a callable that
|
|
138
|
+
* also exposes a `.then` method). The earlier probe only checked
|
|
139
|
+
* `typeof === 'object'`, so those values fell through to the
|
|
140
|
+
* synchronous branch and released the suspend before the async
|
|
141
|
+
* work had even started (round-10 MEDIUM #2).
|
|
142
|
+
* - A throwing `.then` getter. Accessing the property is guarded by
|
|
143
|
+
* try/catch so a pathological value can't escape before the
|
|
144
|
+
* caller's `release()` runs. We deliberately prefer "return false
|
|
145
|
+
* on getter throw" over "propagate" here: the whole point of this
|
|
146
|
+
* helper is to avoid leaking the suspend counter, and the caller
|
|
147
|
+
* treats a non-thenable result as immediately-released.
|
|
148
|
+
*/
|
|
149
|
+
function isThenable(value) {
|
|
150
|
+
if (value == null)
|
|
151
|
+
return false;
|
|
152
|
+
const kind = typeof value;
|
|
153
|
+
if (kind !== 'object' && kind !== 'function')
|
|
154
|
+
return false;
|
|
155
|
+
try {
|
|
156
|
+
return typeof value.then === 'function';
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Parse `MLX_IDLE_CLEAR_CACHE_MS`. Same semantics as the other env
|
|
164
|
+
* knobs in `server.ts`: finite non-negative integer or fall through
|
|
165
|
+
* to the caller's default. A value of `0` explicitly disables the
|
|
166
|
+
* sweeper; negative / non-integer / unparseable values are ignored.
|
|
167
|
+
*/
|
|
168
|
+
export function parseIdleClearCacheEnv() {
|
|
169
|
+
const raw = process.env.MLX_IDLE_CLEAR_CACHE_MS;
|
|
170
|
+
if (raw == null || raw === '')
|
|
171
|
+
return undefined;
|
|
172
|
+
const parsed = Number(raw);
|
|
173
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
174
|
+
return undefined;
|
|
175
|
+
if (!Number.isInteger(parsed))
|
|
176
|
+
return undefined;
|
|
177
|
+
return parsed;
|
|
178
|
+
}
|
|
179
|
+
/** Set to `true` after the first missing-binding warn so we don't spam stderr. */
|
|
180
|
+
let __warnedMissingClearCache = false;
|
|
181
|
+
/**
|
|
182
|
+
* Resolve `__internal__.clearCache` from the loaded `@mlx-node/core`
|
|
183
|
+
* binding, guarded against stale / partial / downgraded `.node` files.
|
|
184
|
+
*
|
|
185
|
+
* Round-5 Finding A: the previous design dereferenced
|
|
186
|
+
* `__internal__.clearCache` at MODULE scope, so the import of
|
|
187
|
+
* `@mlx-node/server` itself threw `TypeError: Cannot read properties
|
|
188
|
+
* of undefined (reading 'clearCache')` if the loaded native binding
|
|
189
|
+
* lacked the namespace. That was unrecoverable from user code —
|
|
190
|
+
* there was no escape hatch, not even `idleClearCacheMs: 0`, because
|
|
191
|
+
* the throw fired before `createServer()` was reached.
|
|
192
|
+
*
|
|
193
|
+
* This resolver probes the namespace defensively at sweeper-creation
|
|
194
|
+
* time (and ONLY when the sweeper is actually enabled, so the
|
|
195
|
+
* `delayMs <= 0` opt-out short-circuits before we even look). Missing
|
|
196
|
+
* namespace / missing function / wrong-type function all route to a
|
|
197
|
+
* one-time `console.warn` + no-op fallback so the server stays up.
|
|
198
|
+
*/
|
|
199
|
+
function resolveClearCache() {
|
|
200
|
+
// Read through the module namespace — `core.__internal__` returns
|
|
201
|
+
// `undefined` when the loaded binding predates the namespace, WITHOUT
|
|
202
|
+
// throwing. Using an optional chain on the function keeps the probe
|
|
203
|
+
// safe even if `__internal__` exists but is some exotic shape.
|
|
204
|
+
const fn = core.__internal__?.clearCache;
|
|
205
|
+
if (typeof fn === 'function') {
|
|
206
|
+
return fn;
|
|
207
|
+
}
|
|
208
|
+
if (!__warnedMissingClearCache) {
|
|
209
|
+
__warnedMissingClearCache = true;
|
|
210
|
+
// Intentionally a `console.warn`, NOT a throw — the server must
|
|
211
|
+
// stay up on a stale native binding; the user's error surface is
|
|
212
|
+
// stderr, not an uncaught exception on a module-import side-effect.
|
|
213
|
+
console.warn('[@mlx-node/server] __internal__.clearCache not found on @mlx-node/core; idle cache drains disabled. Rebuild @mlx-node/core.');
|
|
214
|
+
}
|
|
215
|
+
return () => { };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Create an idle sweeper. Pass `0` or a non-positive value to opt out —
|
|
219
|
+
* the returned object becomes a no-op that still satisfies the
|
|
220
|
+
* interface. Callers can therefore unconditionally wire
|
|
221
|
+
* `beginRequest()` / `endRequest()` without branching on whether the
|
|
222
|
+
* sweeper is enabled.
|
|
223
|
+
*
|
|
224
|
+
* When `onDrain` is omitted, the sweeper resolves
|
|
225
|
+
* `__internal__.clearCache` on `@mlx-node/core` at creation time and
|
|
226
|
+
* caches the result in the returned closure. A missing namespace /
|
|
227
|
+
* function triggers a one-time `console.warn` and a no-op fallback —
|
|
228
|
+
* see `resolveClearCache()`. The `delayMs <= 0` path skips the
|
|
229
|
+
* resolution entirely so the opt-out remains purely passive.
|
|
230
|
+
*/
|
|
231
|
+
export function createIdleSweeper(delayMs, onDrain) {
|
|
232
|
+
if (!Number.isFinite(delayMs) || delayMs <= 0) {
|
|
233
|
+
function passthrough(fn) {
|
|
234
|
+
return fn();
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
beginRequest() { },
|
|
238
|
+
endRequest() { },
|
|
239
|
+
close() { },
|
|
240
|
+
withSuspendedDrains: passthrough,
|
|
241
|
+
suspendDrains() {
|
|
242
|
+
return () => { };
|
|
243
|
+
},
|
|
244
|
+
get isPending() {
|
|
245
|
+
return false;
|
|
246
|
+
},
|
|
247
|
+
get inFlight() {
|
|
248
|
+
return 0;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Resolve the drain callback exactly once per sweeper — either the
|
|
253
|
+
// caller-supplied `onDrain` (used by tests for observability) or the
|
|
254
|
+
// guarded `__internal__.clearCache` lookup. Caching in the closure
|
|
255
|
+
// means the namespace probe runs ONCE per sweeper, not once per
|
|
256
|
+
// timer firing.
|
|
257
|
+
const drainFn = onDrain ?? resolveClearCache();
|
|
258
|
+
let timer = null;
|
|
259
|
+
let inFlight = 0;
|
|
260
|
+
// `loadCounter` tracks active `suspendDrains()` brackets. Any value
|
|
261
|
+
// > 0 means some caller has declared "do not fire a drain right now
|
|
262
|
+
// — a long-running, unbracketed allocator-heavy operation is in
|
|
263
|
+
// progress". `scheduleDrain()` bails when it's positive, and the
|
|
264
|
+
// drain-fire callback re-checks it defensively before calling
|
|
265
|
+
// `drainFn()` in case a suspend arrived between arming and firing
|
|
266
|
+
// on the same event-loop tick.
|
|
267
|
+
let loadCounter = 0;
|
|
268
|
+
const cancelTimer = () => {
|
|
269
|
+
if (timer !== null) {
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
timer = null;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const onTimerFire = () => {
|
|
275
|
+
timer = null;
|
|
276
|
+
// Double-check: if a request arrived between the timer being
|
|
277
|
+
// armed and the callback firing, `inFlight` is non-zero and
|
|
278
|
+
// `beginRequest()` should already have cancelled us. This guard
|
|
279
|
+
// is belt-and-suspenders — shouldn't trip in practice but
|
|
280
|
+
// protects against a timer racing a synchronous
|
|
281
|
+
// `beginRequest()` in adversarial tests.
|
|
282
|
+
if (inFlight !== 0)
|
|
283
|
+
return;
|
|
284
|
+
// Equivalent defensive re-check for the suspend path: a
|
|
285
|
+
// `suspendDrains()` that landed between arming and firing
|
|
286
|
+
// (possible if the arming `setTimeout` callback is queued
|
|
287
|
+
// alongside a synchronous suspend in the same tick) should
|
|
288
|
+
// reschedule rather than drain mid-load. The microtask ordering
|
|
289
|
+
// of `setTimeout` + `clearTimeout` makes this nearly impossible
|
|
290
|
+
// in practice on Node's single-threaded loop — but the check is
|
|
291
|
+
// cheap and future-proofs against timer-pool refactors.
|
|
292
|
+
if (loadCounter > 0) {
|
|
293
|
+
// Re-arm for another window. We intentionally do NOT call
|
|
294
|
+
// `drainFn()` here. If the suspend clears before the next
|
|
295
|
+
// `delayMs` elapses, the disposer returned by `suspendDrains()`
|
|
296
|
+
// will cancel this timer and start fresh anyway.
|
|
297
|
+
timer = setTimeout(onTimerFire, delayMs);
|
|
298
|
+
timer.unref();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
drainFn();
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// Swallow — drain is best-effort and must not crash the
|
|
306
|
+
// server loop. The underlying FFI call can't currently fail
|
|
307
|
+
// but we defend against future refactors growing fallibility.
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const scheduleDrain = () => {
|
|
311
|
+
// Caller must already have confirmed `inFlight === 0`. We
|
|
312
|
+
// defensively null-out the existing timer first so overlapping
|
|
313
|
+
// begin→end→begin→end patterns can't leak a timer.
|
|
314
|
+
cancelTimer();
|
|
315
|
+
// Skip arming while a suspend bracket is active — the matching
|
|
316
|
+
// disposer that takes the counter back to zero will arm a fresh
|
|
317
|
+
// timer on the transition (provided `inFlight === 0`).
|
|
318
|
+
if (loadCounter > 0)
|
|
319
|
+
return;
|
|
320
|
+
timer = setTimeout(onTimerFire, delayMs);
|
|
321
|
+
// Do not keep the event loop alive on this timer alone. If the
|
|
322
|
+
// server would otherwise exit (e.g. the HTTP listener closed),
|
|
323
|
+
// we do not want to force a last-ditch drain.
|
|
324
|
+
timer.unref();
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Suspend drains and return an idempotent, token-scoped disposer.
|
|
328
|
+
* Each call allocates a fresh token; the returned disposer checks
|
|
329
|
+
* the token on every invocation so re-calling the same disposer is
|
|
330
|
+
* a guaranteed no-op. Critically, a *stale* disposer — one that
|
|
331
|
+
* survived its matching release via `withSuspendedDrains` — also
|
|
332
|
+
* cannot re-decrement the counter and mysteriously shift the idle
|
|
333
|
+
* window (round-9 MEDIUM). The `Math.max(0, …)` clamp is retained
|
|
334
|
+
* as defense-in-depth for the impossible case where two different
|
|
335
|
+
* disposers both land after cross-thread reordering.
|
|
336
|
+
*/
|
|
337
|
+
const suspendDrains = () => {
|
|
338
|
+
cancelTimer();
|
|
339
|
+
loadCounter += 1;
|
|
340
|
+
const token = { disposed: false };
|
|
341
|
+
return () => {
|
|
342
|
+
if (token.disposed)
|
|
343
|
+
return;
|
|
344
|
+
token.disposed = true;
|
|
345
|
+
loadCounter = Math.max(0, loadCounter - 1);
|
|
346
|
+
if (loadCounter === 0 && inFlight === 0) {
|
|
347
|
+
scheduleDrain();
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
function withSuspendedDrains(fn) {
|
|
352
|
+
const release = suspendDrains();
|
|
353
|
+
let released = false;
|
|
354
|
+
const releaseOnce = () => {
|
|
355
|
+
if (released)
|
|
356
|
+
return;
|
|
357
|
+
released = true;
|
|
358
|
+
release();
|
|
359
|
+
};
|
|
360
|
+
let result;
|
|
361
|
+
try {
|
|
362
|
+
result = fn();
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
releaseOnce();
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
if (isThenable(result)) {
|
|
369
|
+
// `Promise.resolve` adopts any thenable via the Promise
|
|
370
|
+
// resolution procedure, so a non-Promise thenable (plain
|
|
371
|
+
// object or callable) still hands the caller back a real
|
|
372
|
+
// `Promise<T>`. `.finally` runs on both fulfil and reject.
|
|
373
|
+
return Promise.resolve(result).finally(releaseOnce);
|
|
374
|
+
}
|
|
375
|
+
releaseOnce();
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
beginRequest() {
|
|
380
|
+
inFlight += 1;
|
|
381
|
+
cancelTimer();
|
|
382
|
+
},
|
|
383
|
+
endRequest() {
|
|
384
|
+
if (inFlight === 0) {
|
|
385
|
+
// Defensive: an `endRequest()` without a matching
|
|
386
|
+
// `beginRequest()` is a caller bug. Clamp at zero rather
|
|
387
|
+
// than letting the counter go negative and latch the drain
|
|
388
|
+
// off forever.
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
inFlight -= 1;
|
|
392
|
+
if (inFlight === 0) {
|
|
393
|
+
scheduleDrain();
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
close() {
|
|
397
|
+
cancelTimer();
|
|
398
|
+
},
|
|
399
|
+
withSuspendedDrains,
|
|
400
|
+
suspendDrains,
|
|
401
|
+
get isPending() {
|
|
402
|
+
return timer !== null;
|
|
403
|
+
},
|
|
404
|
+
get inFlight() {
|
|
405
|
+
return inFlight;
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,13 +11,19 @@ export type { ServerConfig, ServerInstance } from './server.js';
|
|
|
11
11
|
* supported public API — names may change without notice.
|
|
12
12
|
*/
|
|
13
13
|
export { parseEnvSeconds as __parseEnvSeconds, parseEnvPositiveInt as __parseEnvPositiveInt } from './server.js';
|
|
14
|
+
export { createIdleSweeper as __createIdleSweeper, parseIdleClearCacheEnv as __parseIdleClearCacheEnv, DEFAULT_IDLE_CLEAR_CACHE_MS as __DEFAULT_IDLE_CLEAR_CACHE_MS, } from './idle-sweeper.js';
|
|
15
|
+
export type { IdleSweeper } from './idle-sweeper.js';
|
|
14
16
|
export { createHandler } from './handler.js';
|
|
15
17
|
export type { HandlerOptions } from './handler.js';
|
|
16
18
|
export { ModelRegistry } from './registry.js';
|
|
17
|
-
export type { ServableModel, ModelEntry, ModelRegistryOptions } from './registry.js';
|
|
19
|
+
export type { ServableModel, ModelEntry, ModelRegistryOptions, RegisterOptions } from './registry.js';
|
|
18
20
|
export { QueueFullError, SessionRegistry } from './session-registry.js';
|
|
19
21
|
export type { SessionLookupResult, SessionRegistryOptions } from './session-registry.js';
|
|
22
|
+
export { resolveServerTuningForUsage } from './timing.js';
|
|
23
|
+
export { QWEN_SAMPLING_DEFAULTS, GEMMA4_SAMPLING_DEFAULTS, LFM2_SAMPLING_DEFAULTS, LAUNCH_PRESETS } from './presets.js';
|
|
24
|
+
export type { LaunchPreset } from './presets.js';
|
|
25
|
+
export type { PublicModelEntry } from './handler.js';
|
|
20
26
|
export type { ResponsesAPIRequest, ResponseObject, ResponseUsage, ResponseError, InputItem, InputMessage, InputFunctionCall, InputFunctionCallOutput, OutputItem, MessageOutputItem, ReasoningOutputItem, FunctionCallOutputItem, OutputTextPart, SummaryTextPart, ResponsesToolDefinition, ContentPart, InputTextPart, StreamEvent, } from './types.js';
|
|
21
|
-
export type { 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';
|
|
27
|
+
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';
|
|
22
28
|
export { writeSSEEvent, beginSSE, endSSE } from './streaming.js';
|
|
23
29
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,17 @@ export { createServer } from './server.js';
|
|
|
10
10
|
* supported public API — names may change without notice.
|
|
11
11
|
*/
|
|
12
12
|
export { parseEnvSeconds as __parseEnvSeconds, parseEnvPositiveInt as __parseEnvPositiveInt } from './server.js';
|
|
13
|
+
export { createIdleSweeper as __createIdleSweeper, parseIdleClearCacheEnv as __parseIdleClearCacheEnv, DEFAULT_IDLE_CLEAR_CACHE_MS as __DEFAULT_IDLE_CLEAR_CACHE_MS, } from './idle-sweeper.js';
|
|
13
14
|
export { createHandler } from './handler.js';
|
|
14
15
|
export { ModelRegistry } from './registry.js';
|
|
15
16
|
export { QueueFullError, SessionRegistry } from './session-registry.js';
|
|
17
|
+
export { resolveServerTuningForUsage } from './timing.js';
|
|
18
|
+
// NOTE: `__resetPromptCacheKeyNonceForTests` is intentionally NOT
|
|
19
|
+
// re-exported here. It is a test-only helper that nukes the module-
|
|
20
|
+
// scoped HMAC nonce (and the once-per-process single-tenant warning
|
|
21
|
+
// flag); exposing it on the public surface would let downstream
|
|
22
|
+
// consumers invalidate every live tier-2 entry with one call. Tests
|
|
23
|
+
// import it from the deep path
|
|
24
|
+
// `packages/server/src/session-registry.js` instead.
|
|
25
|
+
export { QWEN_SAMPLING_DEFAULTS, GEMMA4_SAMPLING_DEFAULTS, LFM2_SAMPLING_DEFAULTS, LAUNCH_PRESETS } from './presets.js';
|
|
16
26
|
export { writeSSEEvent, beginSSE, endSSE } from './streaming.js';
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
/** Anthropic Messages API request → internal `ChatMessage[]` + `ChatConfig`. */
|
|
2
2
|
import type { ChatConfig, ChatMessage } from '@mlx-node/core';
|
|
3
|
-
import type { AnthropicMessagesRequest } from '../types-anthropic.js';
|
|
3
|
+
import type { AnthropicCountTokensRequest, AnthropicMessagesRequest } from '../types-anthropic.js';
|
|
4
4
|
export interface MappedAnthropicRequest {
|
|
5
5
|
messages: ChatMessage[];
|
|
6
6
|
config: ChatConfig;
|
|
7
|
+
/**
|
|
8
|
+
* Client-supplied stop strings (Anthropic `stop_sequences`), normalized to
|
|
9
|
+
* drop absent/empty entries. Carried alongside `config` rather than on it
|
|
10
|
+
* because `ChatConfig` has no native stop field; a downstream consumer is
|
|
11
|
+
* responsible for honouring these.
|
|
12
|
+
*/
|
|
13
|
+
stopSequences: string[];
|
|
7
14
|
}
|
|
8
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Canonicalize the Anthropic `system` field into the same string the mapper
|
|
17
|
+
* bakes into the leading `system` ChatMessage. Used by both
|
|
18
|
+
* `mapAnthropicRequest` (so the model never sees the billing header) and the
|
|
19
|
+
* `/v1/messages` warm-slot gate cache-key derivation (so the gate matches
|
|
20
|
+
* across rotating billing tokens). The two views MUST stay in sync — a
|
|
21
|
+
* single source of truth prevents drift.
|
|
22
|
+
*
|
|
23
|
+
* Asymmetry vs. `mapAnthropicRequest`: the mapper THROWS on non-text blocks
|
|
24
|
+
* (it's a request-validation gate), but this helper silently skips them.
|
|
25
|
+
* Safe because `mapAnthropicRequest` runs first as a pre-flight gate, so by
|
|
26
|
+
* the time the cache-key is computed, the request shape has already been
|
|
27
|
+
* validated.
|
|
28
|
+
*/
|
|
29
|
+
export declare function canonicalizeSystemForCacheKey(system: AnthropicMessagesRequest['system']): string | null;
|
|
30
|
+
export declare function mapAnthropicRequest(req: AnthropicMessagesRequest | AnthropicCountTokensRequest): MappedAnthropicRequest;
|
|
9
31
|
//# sourceMappingURL=anthropic-request.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-request.d.ts","sourceRoot":"","sources":["../../src/mappers/anthropic-request.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAkB,MAAM,gBAAgB,CAAC;AAE9E,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"anthropic-request.d.ts","sourceRoot":"","sources":["../../src/mappers/anthropic-request.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAkB,MAAM,gBAAgB,CAAC;AAE9E,OAAO,KAAK,EAEV,2BAA2B,EAE3B,wBAAwB,EAIzB,MAAM,uBAAuB,CAAC;AAI/B,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;OAKG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAmBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,wBAAwB,CAAC,QAAQ,CAAC,GAAG,MAAM,GAAG,IAAI,CAoBvG;AAiDD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,wBAAwB,GAAG,2BAA2B,GAC1D,sBAAsB,CA8VxB"}
|