@mlx-node/server 0.0.9 → 0.0.12
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 +10 -12
- package/dist/chat-session-warm-reuse.d.ts.map +1 -1
- package/dist/chat-session-warm-reuse.js +10 -12
- package/dist/endpoints/messages.d.ts +2 -2
- package/dist/endpoints/messages.d.ts.map +1 -1
- package/dist/endpoints/messages.js +492 -349
- package/dist/endpoints/responses.d.ts +1 -1
- package/dist/endpoints/responses.d.ts.map +1 -1
- package/dist/endpoints/responses.js +1149 -1055
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +1 -1
- package/dist/health.d.ts +4 -6
- package/dist/health.d.ts.map +1 -1
- package/dist/host/discover.d.ts +1 -2
- package/dist/host/discover.d.ts.map +1 -1
- package/dist/host/discover.js +3 -6
- package/dist/host/index.d.ts +6 -1
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +3 -2
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -9
- package/dist/mappers/anthropic-request.d.ts.map +1 -1
- package/dist/mappers/anthropic-request.js +5 -1
- package/dist/mappers/request.d.ts +12 -2
- package/dist/mappers/request.d.ts.map +1 -1
- package/dist/mappers/request.js +26 -2
- package/dist/model-work-coordinator.d.ts +50 -0
- package/dist/model-work-coordinator.d.ts.map +1 -1
- package/dist/model-work-coordinator.js +161 -0
- package/dist/registry.d.ts +15 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +51 -0
- package/dist/server.d.ts +21 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +30 -6
- package/dist/session-registry.d.ts +266 -86
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +421 -107
- package/dist/streaming.d.ts +37 -2
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +122 -1
- package/dist/transport-visibility.d.ts +5 -4
- package/dist/transport-visibility.d.ts.map +1 -1
- package/dist/transport-visibility.js +5 -4
- package/dist/types-anthropic.d.ts +7 -0
- package/dist/types-anthropic.d.ts.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/presets.d.ts +0 -82
- package/dist/presets.d.ts.map +0 -1
- package/dist/presets.js +0 -98
|
@@ -21,18 +21,13 @@
|
|
|
21
21
|
* by response id — no secondary keying on model name because the
|
|
22
22
|
* registry is already scoped per model.
|
|
23
23
|
*
|
|
24
|
-
* - **Single-warm-session invariant.**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* model is therefore an illusion: at most ONE matches real
|
|
32
|
-
* native state (whichever ran most recently). To prevent
|
|
33
|
-
* cross-session corruption this registry holds at most ONE
|
|
34
|
-
* entry — both `getOrCreate` and `adopt` clear the map before
|
|
35
|
-
* returning or inserting.
|
|
24
|
+
* - **Single-warm-session invariant.** The JS warm registry retains at
|
|
25
|
+
* most ONE `ChatSession` entry — both `getOrCreate` and `adopt` clear
|
|
26
|
+
* the map before returning or inserting. This remains the sole safe
|
|
27
|
+
* reuse mechanism for flat-cache models whose native cache is one
|
|
28
|
+
* mutable vector. Block-paged schedulers instead isolate live turns by
|
|
29
|
+
* cache owner and reuse verified physical blocks through the native
|
|
30
|
+
* prefix table; those models may run fresh JS sessions concurrently.
|
|
36
31
|
*
|
|
37
32
|
* - **Lease semantics on hit.** Clear-on-hit also gives single-
|
|
38
33
|
* flight lease semantics: two overlapping requests referencing
|
|
@@ -43,14 +38,13 @@
|
|
|
43
38
|
* `ChatSession`'s single-flight "concurrent send() not allowed"
|
|
44
39
|
* guard.
|
|
45
40
|
*
|
|
46
|
-
* - **
|
|
47
|
-
*
|
|
48
|
-
* `getOrCreate` compares the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* — the registry does not care which is which.
|
|
41
|
+
* - **Prefix compatibility changes miss.** Each entry records its
|
|
42
|
+
* `instructions` plus an opaque fingerprint of the cache salt used to
|
|
43
|
+
* adopt it. `getOrCreate` compares both against the new request;
|
|
44
|
+
* mismatch forces owner release and cold replay instead of reusing a
|
|
45
|
+
* stale prompt or changing the security domain of one live native
|
|
46
|
+
* request. OpenAI `instructions` and Anthropic `system` share the same
|
|
47
|
+
* parameter; both endpoints also thread their mapped `cache_salt`.
|
|
54
48
|
*
|
|
55
49
|
* - **Cache miss fallback.** On a miss (eviction, interleaved turn
|
|
56
50
|
* on a different chain, restart, lease-on-hit) the endpoint
|
|
@@ -73,32 +67,20 @@
|
|
|
73
67
|
* concurrent mutation by design. `sweep()` can be scheduled
|
|
74
68
|
* via `setInterval` without colliding with in-flight calls.
|
|
75
69
|
*
|
|
76
|
-
* - **Per-model
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* cache is a single mutable resource and two parallel
|
|
83
|
-
* `primeHistory()` / `send*()` calls would race. Whichever
|
|
84
|
-
* finished last would win `adopt()`, poisoning the hot path
|
|
85
|
-
* for every subsequent chained turn.
|
|
86
|
-
*
|
|
87
|
-
* `withExclusive(fn)` serializes every per-model dispatch via
|
|
88
|
-
* a FIFO `execLock` chain. `/v1/responses` and `/v1/messages`
|
|
89
|
-
* wrap the full `getOrCreate -> run -> adopt/drop` span in one
|
|
90
|
-
* `withExclusive` so at most one request holds the model at a
|
|
91
|
-
* time. A weaker epoch-token scheme would let the losing
|
|
92
|
-
* `adopt()` no-op but the native KV would already be wrong.
|
|
70
|
+
* - **Per-model admission lane.** Flat and not-yet-batched families use
|
|
71
|
+
* `withExclusive(fn)`, the original FIFO mutex across the full dispatch.
|
|
72
|
+
* A model that explicitly reports `maxConcurrentSequences() > 1` uses
|
|
73
|
+
* `withAdmission(fn)`, a counting semaphore sized to that native
|
|
74
|
+
* scheduler. Per-session serialization still lives in `ChatSession`;
|
|
75
|
+
* only independent sessions share the model lane.
|
|
93
76
|
*/
|
|
94
77
|
import type { ChatConfig } from '@mlx-node/core';
|
|
95
78
|
import { ChatSession, type SessionCapableModel } from '@mlx-node/lm';
|
|
96
79
|
/**
|
|
97
|
-
* Test-only hook used by the scoping unit tests to simulate a
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* dedupe cache so tests can re-exercise the once-per-key diagnostic
|
|
101
|
-
* path.
|
|
80
|
+
* Test-only hook used by the scoping unit tests to simulate a server
|
|
81
|
+
* restart: resets the module-scoped HMAC nonce (so every previously stored
|
|
82
|
+
* tier-2 key and cache-salt fingerprint misses) and clears the silent-miss
|
|
83
|
+
* dedupe cache so tests can re-exercise the once-per-key diagnostic path.
|
|
102
84
|
*
|
|
103
85
|
* **Not exported from the package's public `index.ts` surface** —
|
|
104
86
|
* exporting it there would let downstream consumers nuke tier-2
|
|
@@ -152,11 +134,21 @@ export interface SessionRegistryOptions {
|
|
|
152
134
|
* synchronously so the endpoint layer can emit HTTP 429 and the
|
|
153
135
|
* client can retry later.
|
|
154
136
|
*
|
|
155
|
-
* Default: `undefined` (unbounded —
|
|
156
|
-
*
|
|
157
|
-
*
|
|
137
|
+
* Default at THIS layer: `undefined` (unbounded) — a directly
|
|
138
|
+
* constructed `SessionRegistry` has no cap. `createServer` supplies
|
|
139
|
+
* its own default of 16 per {@link ServerConfig.maxQueueDepthPerModel}
|
|
140
|
+
* (or the `MLX_MAX_QUEUE_DEPTH_PER_MODEL` env var; config
|
|
141
|
+
* `'unbounded'` opts out), so server-allocated registries are capped
|
|
142
|
+
* unless the operator explicitly opted out.
|
|
158
143
|
*/
|
|
159
144
|
maxQueueDepth?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Number of independent dispatches the native model scheduler can advance
|
|
147
|
+
* concurrently. Values below two retain the legacy exclusive FIFO. The
|
|
148
|
+
* model registry derives this from `maxConcurrentSequences()`; direct
|
|
149
|
+
* construction defaults to one.
|
|
150
|
+
*/
|
|
151
|
+
maxConcurrentDispatches?: number;
|
|
160
152
|
/**
|
|
161
153
|
* Optional sampling defaults applied to every `ChatSession` this
|
|
162
154
|
* registry allocates. Forwarded verbatim into `new ChatSession(model,
|
|
@@ -184,9 +176,33 @@ export interface SessionRegistryOptions {
|
|
|
184
176
|
* can reliably catch it without racing the chain.
|
|
185
177
|
*/
|
|
186
178
|
export declare class QueueFullError extends Error {
|
|
187
|
-
readonly
|
|
179
|
+
readonly queueDepth: number;
|
|
180
|
+
readonly preDispatchAdmissions: number;
|
|
181
|
+
readonly admissionFootprint: number;
|
|
188
182
|
readonly limit: number;
|
|
189
|
-
constructor(
|
|
183
|
+
constructor(queueDepth: number, preDispatchAdmissions: number, limit: number);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Admission permit returned by
|
|
187
|
+
* {@link SessionRegistry.beginPreDispatchAdmission}. Represents exactly
|
|
188
|
+
* ONE unit of the per-model admission budget, and ends in exactly one of
|
|
189
|
+
* two terminal states:
|
|
190
|
+
*
|
|
191
|
+
* - **handed off**: passed as the second argument of
|
|
192
|
+
* `withExclusive(fn, permit)` or `withAdmission(fn, permit)`, which
|
|
193
|
+
* consumes it atomically as that
|
|
194
|
+
* call's admission — the unit converts into the waiter charge (or is
|
|
195
|
+
* retired when the caller wins the runner slot). `release()` becomes
|
|
196
|
+
* a no-op afterwards.
|
|
197
|
+
* - **released**: `release()` frees the unit without a dispatch. Every
|
|
198
|
+
* exit between admission and lock placement must do this — the
|
|
199
|
+
* recommended pattern is one unconditional `release()` in a
|
|
200
|
+
* `finally`, which is safe on every path because `release()` is
|
|
201
|
+
* idempotent and a no-op after handoff.
|
|
202
|
+
*/
|
|
203
|
+
export interface PreDispatchAdmission {
|
|
204
|
+
/** Idempotent: free the budget unit unless already handed off. */
|
|
205
|
+
release(): void;
|
|
190
206
|
}
|
|
191
207
|
/**
|
|
192
208
|
* Result of {@link SessionRegistry.getOrCreate}. `hit` reflects whether
|
|
@@ -203,6 +219,7 @@ export declare class SessionRegistry {
|
|
|
203
219
|
private readonly model;
|
|
204
220
|
private readonly ttlSec;
|
|
205
221
|
private readonly maxQueueDepth;
|
|
222
|
+
private readonly maxConcurrentDispatches;
|
|
206
223
|
/**
|
|
207
224
|
* Per-model sampling defaults forwarded into every new `ChatSession`
|
|
208
225
|
* via its `defaultConfig` constructor option. `undefined` preserves
|
|
@@ -232,6 +249,25 @@ export declare class SessionRegistry {
|
|
|
232
249
|
* runner-slot caller against the waiter cap.
|
|
233
250
|
*/
|
|
234
251
|
private queuedCount;
|
|
252
|
+
/**
|
|
253
|
+
* Requests admitted by {@link beginPreDispatchAdmission} whose permit
|
|
254
|
+
* is still outstanding — parked in the `ModelWorkCoordinator` writer
|
|
255
|
+
* queue (host mode), blocked in pre-lock store lookups
|
|
256
|
+
* (`previous_response_id` continuations), or anywhere else between the
|
|
257
|
+
* endpoint gate and resident-lane placement. None of that parking is
|
|
258
|
+
* visible to `queuedCount`; this counter is what lets the gate bound
|
|
259
|
+
* it. Decremented ONLY by the permit itself: `release()` on a bail-out
|
|
260
|
+
* or the atomic consume inside the selected lane on handoff.
|
|
261
|
+
*/
|
|
262
|
+
private preDispatchAdmits;
|
|
263
|
+
/**
|
|
264
|
+
* Consume hooks for outstanding permits, keyed by permit identity.
|
|
265
|
+
* Registry-scoped on purpose: both execution lanes consult THIS map, so a
|
|
266
|
+
* permit minted by a different registry is simply not found and the
|
|
267
|
+
* call falls back to normal waiter charging — a cross-registry handoff
|
|
268
|
+
* cannot corrupt either registry's counters.
|
|
269
|
+
*/
|
|
270
|
+
private readonly permitConsumers;
|
|
235
271
|
/**
|
|
236
272
|
* Holds AT MOST ONE entry under the single-warm invariant (see the
|
|
237
273
|
* module-level rustdoc). `getOrCreate` and `adopt` both clear the
|
|
@@ -240,6 +276,15 @@ export declare class SessionRegistry {
|
|
|
240
276
|
* turn on another cached entry.
|
|
241
277
|
*/
|
|
242
278
|
private readonly entries;
|
|
279
|
+
/**
|
|
280
|
+
* Eviction is synchronous at the map boundary, but releasing a native
|
|
281
|
+
* scheduler owner is asynchronous. Start every disposal immediately and
|
|
282
|
+
* retain its promise so endpoint admission lanes can wait for command-order
|
|
283
|
+
* visibility before dispatching a replacement turn.
|
|
284
|
+
*/
|
|
285
|
+
private readonly pendingDisposals;
|
|
286
|
+
private readonly disposalBySession;
|
|
287
|
+
private readonly failedDisposals;
|
|
243
288
|
/**
|
|
244
289
|
* Shared sentinel representing "the execution chain is idle" — a
|
|
245
290
|
* pre-resolved promise. `execLock` starts at this value and is
|
|
@@ -265,6 +310,10 @@ export declare class SessionRegistry {
|
|
|
265
310
|
* cleanly from the idle state.
|
|
266
311
|
*/
|
|
267
312
|
private execLock;
|
|
313
|
+
/** Active holders in the continuous-batching admission lane. */
|
|
314
|
+
private activeAdmissions;
|
|
315
|
+
/** FIFO waiters parked behind the continuous-batching admission limit. */
|
|
316
|
+
private readonly admissionWaiters;
|
|
268
317
|
constructor(opts: SessionRegistryOptions);
|
|
269
318
|
/**
|
|
270
319
|
* Construct a fresh `ChatSession` bound to this registry's model and
|
|
@@ -275,19 +324,117 @@ export declare class SessionRegistry {
|
|
|
275
324
|
* pinned sampling knobs by picking a cold-replay path.
|
|
276
325
|
*/
|
|
277
326
|
private newSession;
|
|
327
|
+
private scheduleDispose;
|
|
328
|
+
/**
|
|
329
|
+
* Dispose one leased session while retaining failed cleanup for a later
|
|
330
|
+
* registry flush. `ChatSession.dispose()` removes successful owners as it
|
|
331
|
+
* goes, so a retry only revisits owners whose native release failed.
|
|
332
|
+
*/
|
|
333
|
+
disposeSession(session: ChatSession<SessionCapableModel>): Promise<void>;
|
|
334
|
+
/** Remove every cached entry except an optional session being leased. */
|
|
335
|
+
private evictEntriesExcept;
|
|
336
|
+
/**
|
|
337
|
+
* Wait until every disposal scheduled so far has settled, retrying each
|
|
338
|
+
* failure discovered during this flush once. A persistent failure remains
|
|
339
|
+
* recorded for a later flush instead of spinning forever. Disposals log and
|
|
340
|
+
* absorb their own failures so cleanup cannot rewrite a response that has
|
|
341
|
+
* already reached the client.
|
|
342
|
+
*/
|
|
343
|
+
flushPendingDisposals(): Promise<void>;
|
|
278
344
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
345
|
+
* Disposals started but not yet settled — each one is an in-flight
|
|
346
|
+
* native `releaseCacheOwner` round-trip — plus disposals whose initial
|
|
347
|
+
* attempt and bounded retry both failed and remain owed to the native
|
|
348
|
+
* scheduler ({@link failedDisposals}), retried by the next
|
|
349
|
+
* {@link flushPendingDisposals}. Endpoints await that flush before
|
|
350
|
+
* leaving the admission lane, but it runs after the response has
|
|
351
|
+
* finished, so an observer keyed on request completion can still beat
|
|
352
|
+
* the release. `adopt`/`drop`/`sweep` schedule synchronously, so once
|
|
353
|
+
* the request counters read zero any disposal those requests will ever
|
|
354
|
+
* cause is already counted here. Primarily for diagnostics/tests.
|
|
355
|
+
*/
|
|
356
|
+
get pendingDisposalCount(): number;
|
|
357
|
+
/**
|
|
358
|
+
* Number of requests waiting for this model's selected admission lane.
|
|
359
|
+
* Active dispatches are not included. Primarily for diagnostics/tests.
|
|
282
360
|
*/
|
|
283
361
|
get queueDepth(): number;
|
|
284
362
|
/**
|
|
285
|
-
* Configured waiter cap for this model's
|
|
363
|
+
* Configured waiter cap for this model's admission lane, or `undefined`
|
|
286
364
|
* when unbounded. Paired with {@link queueDepth} so a readiness probe can
|
|
287
365
|
* tell "3 waiters, unbounded" (fine) from "3 waiters, cap of 3" (the next
|
|
288
366
|
* request gets a 429) without reaching into private state.
|
|
289
367
|
*/
|
|
290
368
|
get queueDepthLimit(): number | undefined;
|
|
369
|
+
/** Native continuous-batching capacity used by the endpoint route switch. */
|
|
370
|
+
get concurrentAdmissionLimit(): number;
|
|
371
|
+
/**
|
|
372
|
+
* Outstanding pre-dispatch permits — requests admitted by
|
|
373
|
+
* {@link beginPreDispatchAdmission} that have neither handed their
|
|
374
|
+
* permit to the selected execution lane nor released it yet. These permits,
|
|
375
|
+
* queued callers, and active dispatches share one bounded budget; see
|
|
376
|
+
* {@link assertAdmissionCapacity}. For probes and diagnostics.
|
|
377
|
+
*/
|
|
378
|
+
get preDispatchAdmitCount(): number;
|
|
379
|
+
/**
|
|
380
|
+
* Single source of truth for the per-model admission budget — the
|
|
381
|
+
* ONLY place the cap arithmetic lives. Every admission path calls
|
|
382
|
+
* this: {@link beginPreDispatchAdmission} before minting a permit,
|
|
383
|
+
* and both execution lanes for every non-handed-off caller. A handed-off
|
|
384
|
+
* permit skips the call for its OWN
|
|
385
|
+
* token only: that token was charged here at acquisition and its
|
|
386
|
+
* conversion keeps the total constant, so re-checking would
|
|
387
|
+
* double-charge an already-admitted request. It never exempts anyone
|
|
388
|
+
* else — all other outstanding state stays counted for every caller
|
|
389
|
+
* that did not pay.
|
|
390
|
+
*
|
|
391
|
+
* Budget invariant: active dispatches + queued callers + outstanding
|
|
392
|
+
* permits never exceed `maxConcurrentDispatches + maxQueueDepth`.
|
|
393
|
+
* On the exclusive lane `maxConcurrentDispatches` is one, exactly the
|
|
394
|
+
* original runner entitlement. On the batched lane it is the native
|
|
395
|
+
* scheduler's sequence capacity.
|
|
396
|
+
*
|
|
397
|
+
* Charging stays at the call sites (`preDispatchAdmits += 1` at the
|
|
398
|
+
* gate, `queuedCount += 1` for waiters, and the selected lane's active
|
|
399
|
+
* count for runners); every admitted unit is counted by exactly one
|
|
400
|
+
* at any time, which is what makes the footprint sum complete across
|
|
401
|
+
* any interleaving of permitted and permitless callers.
|
|
402
|
+
*
|
|
403
|
+
* Throws {@link QueueFullError} — reporting the footprint and the
|
|
404
|
+
* cap — when the caller does not fit; returns normally otherwise.
|
|
405
|
+
* No-op when the registry is unbounded.
|
|
406
|
+
*/
|
|
407
|
+
private assertAdmissionCapacity;
|
|
408
|
+
/**
|
|
409
|
+
* Endpoint-side early admission against this registry's cap, taken BEFORE
|
|
410
|
+
* the request enters any pre-dispatch parking spot the resident lane cannot
|
|
411
|
+
* see. Resident host traffic deliberately bypasses the model-load writer so
|
|
412
|
+
* continuous batching remains reachable, but a continuation can still block
|
|
413
|
+
* in `await store.getChain(...)` (or other pre-lock work) with `queuedCount`
|
|
414
|
+
* unchanged. This permit keeps all such work inside the same bounded budget.
|
|
415
|
+
*
|
|
416
|
+
* Accounting: pre-dispatch permits, active dispatches, and queued callers
|
|
417
|
+
* draw from ONE budget: `maxQueueDepth` waiter slots plus the selected
|
|
418
|
+
* lane's active capacity. Both lanes and this early gate route through
|
|
419
|
+
* {@link assertAdmissionCapacity}, so permitted and permitless arrivals
|
|
420
|
+
* cannot double-spend a slot.
|
|
421
|
+
*
|
|
422
|
+
* Throws {@link QueueFullError} synchronously when over cap (the
|
|
423
|
+
* caller maps it to the same 429 envelope as resident-lane rejection
|
|
424
|
+
* reject). On admission returns a {@link PreDispatchAdmission} permit
|
|
425
|
+
* the caller must RETAIN through ALL pre-lock asynchronous work and
|
|
426
|
+
* then hand to `withExclusive(fn, permit)` or `withAdmission(fn, permit)`,
|
|
427
|
+
* which consumes it
|
|
428
|
+
* atomically as that call's admission — one budget, one token per
|
|
429
|
+
* request, never double-counted. Releasing the permit early instead
|
|
430
|
+
* of handing it off re-opens the hole this gate closes: the request
|
|
431
|
+
* would be counted by NEITHER counter while parked, arrivals would
|
|
432
|
+
* refill the budget, and the resident lane would then admit a second
|
|
433
|
+
* full waiter budget on top. `release()` belongs on bail-out paths
|
|
434
|
+
* only (idempotent, no-op after handoff — an unconditional `finally`
|
|
435
|
+
* release is the recommended shape).
|
|
436
|
+
*/
|
|
437
|
+
beginPreDispatchAdmission(): PreDispatchAdmission;
|
|
291
438
|
/**
|
|
292
439
|
* Current sampling defaults applied to every new `ChatSession` this
|
|
293
440
|
* registry allocates. Exposed primarily for tests and diagnostics.
|
|
@@ -316,11 +463,11 @@ export declare class SessionRegistry {
|
|
|
316
463
|
*
|
|
317
464
|
* 1. **Tier 1 — `previousResponseId`.** The existing hot path:
|
|
318
465
|
* exact id match on a live, non-expired entry whose stored
|
|
319
|
-
* `instructions`
|
|
466
|
+
* `instructions` and cache-salt fingerprint match the request. On
|
|
320
467
|
* a match the entry is leased out (single-use: removed from the
|
|
321
468
|
* map so a concurrent second request cannot share the live
|
|
322
|
-
* `ChatSession`). On a miss — unknown id, expired,
|
|
323
|
-
*
|
|
469
|
+
* `ChatSession`). On a miss — unknown id, expired, instructions
|
|
470
|
+
* drift, or cache-salt drift — the method falls through to a FRESH
|
|
324
471
|
* session regardless of whether tier 2 would have hit.
|
|
325
472
|
*
|
|
326
473
|
* `previousResponseId` wins unconditionally when supplied. The
|
|
@@ -340,9 +487,9 @@ export declare class SessionRegistry {
|
|
|
340
487
|
* is to key on the client-supplied `prompt_cache_key`. Scans
|
|
341
488
|
* for any live, non-expired entry whose stored
|
|
342
489
|
* `promptCacheKey` is non-null AND byte-equal to the caller's
|
|
343
|
-
* `promptCacheKey
|
|
344
|
-
*
|
|
345
|
-
* `null` — an opt-out sentinel from a client that forgot to
|
|
490
|
+
* `promptCacheKey`, whose stored `instructions` are byte-equal,
|
|
491
|
+
* AND whose cache-salt fingerprint matches. Empty string is treated
|
|
492
|
+
* as a distinct key from `null` — an opt-out sentinel from a client that forgot to
|
|
346
493
|
* thread the key must NOT collide with another client that
|
|
347
494
|
* did set it to empty. On a match the entry is leased out
|
|
348
495
|
* (same single-use semantics as tier 1). On a miss, fall
|
|
@@ -358,7 +505,7 @@ export declare class SessionRegistry {
|
|
|
358
505
|
* native `cachedTokens > 0` confirms the prefix-cache machinery
|
|
359
506
|
* actually reused the cached tokens).
|
|
360
507
|
*/
|
|
361
|
-
getOrCreate(previousResponseId: string | null, requestedInstructions: string | null, promptCacheKey?: string | null): SessionLookupResult;
|
|
508
|
+
getOrCreate(previousResponseId: string | null, requestedInstructions: string | null, promptCacheKey?: string | null, requestedCacheSalt?: string | null): SessionLookupResult;
|
|
362
509
|
/**
|
|
363
510
|
* Allocate a fresh `ChatSession` bound to this registry's model
|
|
364
511
|
* without touching the warm slot. Intended for the `/v1/messages`
|
|
@@ -423,17 +570,18 @@ export declare class SessionRegistry {
|
|
|
423
570
|
* registry's own warm slot.
|
|
424
571
|
*
|
|
425
572
|
* Behaviour: walk the registry's at-most-one warm entry. If it is
|
|
426
|
-
* non-expired
|
|
427
|
-
* `requestedInstructions`,
|
|
428
|
-
*
|
|
429
|
-
*
|
|
573
|
+
* non-expired, its stored `instructions` are byte-equal to
|
|
574
|
+
* `requestedInstructions`, AND its stored cache salt equals
|
|
575
|
+
* `requestedCacheSalt`, lease it out (single-use — `entries.clear()`
|
|
576
|
+
* before return, mirroring the tier-1 / tier-2 lease-on-hit semantics).
|
|
577
|
+
* Otherwise clear the map and return a fresh session.
|
|
430
578
|
*
|
|
431
579
|
* Crucially, this lookup IGNORES `entry.promptCacheKey` and ignores
|
|
432
580
|
* the entry's prior `previousResponseId` keying — any warm slot is
|
|
433
|
-
* fair game for `/v1/messages` reuse.
|
|
434
|
-
*
|
|
435
|
-
* forces cold replay
|
|
436
|
-
*
|
|
581
|
+
* fair game for `/v1/messages` reuse. Byte-equal instructions and cache
|
|
582
|
+
* salt are the correctness gates: a system prompt or prefix-cache
|
|
583
|
+
* security-domain change forces cold replay instead of reusing stale state
|
|
584
|
+
* or asking the native adapter to mutate a live request's salt.
|
|
437
585
|
*
|
|
438
586
|
* **Adoption sentinel.** `/v1/messages` adopts back under the literal
|
|
439
587
|
* sentinel id `'__msg_warm__'`. That sentinel will never appear as a
|
|
@@ -469,7 +617,7 @@ export declare class SessionRegistry {
|
|
|
469
617
|
* `responses.ts` (around the `runSessionNonStreaming` /
|
|
470
618
|
* `runSessionStreaming` branches) describes.
|
|
471
619
|
*/
|
|
472
|
-
getOrCreateWarmAny(requestedInstructions: string | null): SessionLookupResult;
|
|
620
|
+
getOrCreateWarmAny(requestedInstructions: string | null, requestedCacheSalt?: string | null): SessionLookupResult;
|
|
473
621
|
/**
|
|
474
622
|
* Insert a session under a newly allocated response id. Clears the
|
|
475
623
|
* map before inserting to keep the single-warm invariant explicit
|
|
@@ -478,6 +626,8 @@ export declare class SessionRegistry {
|
|
|
478
626
|
* `instructions` is the prefix/system state used for this turn;
|
|
479
627
|
* stored on the entry and compared on the next `getOrCreate` to
|
|
480
628
|
* detect prefix changes that must force a cold replay.
|
|
629
|
+
* `cacheSalt` is HMAC-fingerprinted before storage and compared alongside
|
|
630
|
+
* the prefix so the raw security-domain key is not retained.
|
|
481
631
|
*
|
|
482
632
|
* `promptCacheKey` is the client-supplied conversation-chain key
|
|
483
633
|
* that enables the registry's tier-2 lookup for stateless agent
|
|
@@ -488,7 +638,7 @@ export declare class SessionRegistry {
|
|
|
488
638
|
* key-equality on both sides can hit tier 2). See
|
|
489
639
|
* {@link SessionRegistry.getOrCreate} for the precedence rules.
|
|
490
640
|
*/
|
|
491
|
-
adopt(responseId: string, session: ChatSession<SessionCapableModel>, instructions: string | null, promptCacheKey?: string | null | undefined): void;
|
|
641
|
+
adopt(responseId: string, session: ChatSession<SessionCapableModel>, instructions: string | null, promptCacheKey?: string | null | undefined, cacheSalt?: string | null | undefined): void;
|
|
492
642
|
/**
|
|
493
643
|
* Remove a session by response id. No-op if the key is not present.
|
|
494
644
|
*/
|
|
@@ -515,33 +665,63 @@ export declare class SessionRegistry {
|
|
|
515
665
|
* the new tail, awaits the old tail, then runs `fn`. The
|
|
516
666
|
* `finally` releases regardless of whether `fn` threw.
|
|
517
667
|
*
|
|
518
|
-
* **Admission control.**
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
668
|
+
* **Admission control.** Every non-handed-off call — waiter AND
|
|
669
|
+
* runner — routes through {@link assertAdmissionCapacity}, the single
|
|
670
|
+
* source of truth for the budget: the combined admission footprint
|
|
671
|
+
* (`queuedCount + preDispatchAdmits`) plus this caller must fit
|
|
672
|
+
* within `maxQueueDepth` plus the idle-chain runner entitlement.
|
|
673
|
+
* Over-budget calls throw {@link QueueFullError} — SYNCHRONOUSLY
|
|
674
|
+
* from the caller's perspective, not merely before `await prev`. The
|
|
675
|
+
* wrapper is deliberately NOT declared `async` so the admission gate
|
|
676
|
+
* throws on the caller's stack frame, letting endpoint handlers wrap
|
|
677
|
+
* the call site in a plain try/catch without racing promise
|
|
526
678
|
* microtasks. On acceptance the async body takes over via the
|
|
527
679
|
* returned `Promise<T>`.
|
|
528
680
|
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
* default (undefined) preserves
|
|
681
|
+
* A cap of N permits one running dispatch plus N admitted-but-not-
|
|
682
|
+
* running requests (queued waiters and outstanding permits
|
|
683
|
+
* combined), rejecting the next. The default (undefined) preserves
|
|
684
|
+
* the original unbounded behaviour.
|
|
532
685
|
*
|
|
533
686
|
* **Runner-slot admission.** Whether a given caller counts as the
|
|
534
687
|
* runner slot or as a waiter is decided up front by comparing
|
|
535
688
|
* `execLock` against the idle sentinel `initialLock`. If they are
|
|
536
|
-
* identical, nobody is currently in-flight and this caller
|
|
537
|
-
*
|
|
538
|
-
* never touches `queuedCount`. Otherwise it is a waiter
|
|
539
|
-
* normal
|
|
540
|
-
* what keeps a synchronous burst such as `Promise.all([fn, fn])`
|
|
689
|
+
* identical, nobody is currently in-flight and this caller is
|
|
690
|
+
* admitted against the extra runner entitlement rather than a waiter
|
|
691
|
+
* slot — it never touches `queuedCount`. Otherwise it is a waiter
|
|
692
|
+
* and the normal charge / increment / decrement cycle applies. This
|
|
693
|
+
* is what keeps a synchronous burst such as `Promise.all([fn, fn])`
|
|
541
694
|
* admissible under `maxQueueDepth = 1` — Call 1 is the runner,
|
|
542
|
-
* Call 2 is the one allowed waiter, Call 3 would throw.
|
|
695
|
+
* Call 2 is the one allowed waiter, Call 3 would throw. The runner
|
|
696
|
+
* path is NOT exempt from the budget: while the chain is idle,
|
|
697
|
+
* {@link beginPreDispatchAdmission} lends out `maxQueueDepth + 1`
|
|
698
|
+
* permits precisely because one of them is entitled to become the
|
|
699
|
+
* runner, so a permitless call that would take that seat while the
|
|
700
|
+
* whole runner-plus-waiter capacity is already spoken for must
|
|
701
|
+
* reject — otherwise the outstanding permits would later convert on
|
|
702
|
+
* top of it and breach the cap.
|
|
703
|
+
*
|
|
704
|
+
* **Permit handoff.** A caller that was already admitted by
|
|
705
|
+
* {@link beginPreDispatchAdmission} passes its permit as the second
|
|
706
|
+
* argument; the permit is consumed atomically as this call's
|
|
707
|
+
* admission instead of charging `queuedCount` a second time — one
|
|
708
|
+
* budget, one token per request. See the handoff comment in the
|
|
709
|
+
* body and {@link PreDispatchAdmission}.
|
|
710
|
+
*/
|
|
711
|
+
withExclusive<T>(fn: () => Promise<T>, permit?: PreDispatchAdmission): Promise<T>;
|
|
712
|
+
/**
|
|
713
|
+
* Admit one dispatch to the model's continuous-batching lane.
|
|
714
|
+
*
|
|
715
|
+
* Admission rejection is synchronous, matching {@link withExclusive} and
|
|
716
|
+
* preserving the endpoint's existing QueueFullError-to-429 mapping. Once
|
|
717
|
+
* accepted, at most {@link concurrentAdmissionLimit} closures run at once;
|
|
718
|
+
* excess accepted callers wait FIFO and contribute to {@link queueDepth}.
|
|
719
|
+
* A pre-dispatch permit is consumed atomically into either an active slot or
|
|
720
|
+
* a queued slot, so the early endpoint gate and this semaphore share one
|
|
721
|
+
* bounded budget.
|
|
543
722
|
*/
|
|
544
|
-
|
|
723
|
+
withAdmission<T>(fn: () => Promise<T>, permit?: PreDispatchAdmission): Promise<T>;
|
|
724
|
+
private _runAdmission;
|
|
545
725
|
/**
|
|
546
726
|
* Async tail of {@link withExclusive}. Kept separate so the public
|
|
547
727
|
* wrapper stays a plain (non-async) function whose admission-gate
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AA+ErE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kCAAkC,IAAI,IAAI,CAGzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,CAE3D;AAuDD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CA2BzF;AAED,uDAAuD;AACvD,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,KAAK,EAAE,mBAAmB,CAAC;IAC3B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;OAcG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,UAAU,CAAC;IAC9B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,YAAY,UAAU,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAW3E;CACF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,WAAW,CAAC,mBAAmB,CAAC,CAAC;IAC1C,GAAG,EAAE,OAAO,CAAC;CACd;AA6CD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAS;IACjD;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,eAAe,CAAqB;IAC5C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,WAAW,CAAK;IACxB;;;;;;;;;OASG;IACH,OAAO,CAAC,iBAAiB,CAAK;IAC9B;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsD;IACtF;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;IAChE;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA4B;IAC7D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAkE;IACpG,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA+C;IAC/E;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;IAChE;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,QAAQ,CAAmC;IACnD,gEAAgE;IAChE,OAAO,CAAC,gBAAgB,CAAK;IAC7B,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyB;IAE1D,YAAY,IAAI,EAAE,sBAAsB,EASvC;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,eAAe;IAevB;;;;OAIG;IACG,cAAc,CAAC,OAAO,EAAE,WAAW,CAAC,mBAAmB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ7E;IAED,yEAAyE;IACzE,OAAO,CAAC,kBAAkB;IAO1B;;;;;;OAMG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAY3C;IAED;;;;;;;;;;;OAWG;IACH,IAAI,oBAAoB,IAAI,MAAM,CAEjC;IAED;;;OAGG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;;;OAKG;IACH,IAAI,eAAe,IAAI,MAAM,GAAG,SAAS,CAExC;IAED,6EAA6E;IAC7E,IAAI,wBAAwB,IAAI,MAAM,CAErC;IAED;;;;;;OAMG;IACH,IAAI,qBAAqB,IAAI,MAAM,CAElC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,OAAO,CAAC,uBAAuB;IAU/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,yBAAyB,IAAI,oBAAoB,CAmBhD;IAED;;;OAGG;IACH,IAAI,qBAAqB,IAAI,UAAU,GAAG,SAAS,CAElD;IAED,IAAI,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAEzC;IAED;;;;;;;;OAQG;IACH,mBAAmB,CAAC,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,CAE1D;IAED,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAElD;IAED,+FAA+F;IAC/F,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkDG;IACH,WAAW,CACT,kBAAkB,EAAE,MAAM,GAAG,IAAI,EACjC,qBAAqB,EAAE,MAAM,GAAG,IAAI,EACpC,cAAc,GAAE,MAAM,GAAG,IAAW,EACpC,kBAAkB,GAAE,MAAM,GAAG,IAAW,GACvC,mBAAmB,CA4ErB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,kBAAkB,IAAI,mBAAmB,CAExC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+EG;IACH,kBAAkB,CAChB,qBAAqB,EAAE,MAAM,GAAG,IAAI,EACpC,kBAAkB,GAAE,MAAM,GAAG,IAAW,GACvC,mBAAmB,CAmBrB;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,CACH,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,WAAW,CAAC,mBAAmB,CAAC,EACzC,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,cAAc,GAAE,MAAM,GAAG,IAAI,GAAG,SAAgB,EAChD,SAAS,GAAE,MAAM,GAAG,IAAI,GAAG,SAAgB,GAC1C,IAAI,CAeN;IAED;;OAEG;IACH,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAK7B;IAED;;;;OAIG;IACH,KAAK,IAAI,IAAI,CAQZ;IAED,2DAA2D;IAC3D,KAAK,IAAI,IAAI,CAEZ;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwDG;IACH,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,CAAC,CAAC,CAqDhF;IAED;;;;;;;;;;OAUG;IACH,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,CAAC,CAAC,CAwBhF;YAEa,aAAa;IAY3B;;;;;;;;;;OAUG;YACW,aAAa;CAiD5B"}
|