import { C as Clock, D as Decision, L as Limiter } from './types-DKirIBQt.js'; import { R as RedisClientLike } from './store-CQjuAFM_.js'; interface AdaptiveConcurrencyOptions { /** Hard floor on the inferred ceiling. Default 4. */ minLimit?: number; /** Hard ceiling on the inferred ceiling. Default 512. */ maxLimit?: number; /** Where the estimate starts. Default = `minLimit`. */ initialLimit?: number; /** Which inference law drives the limit. Default `"gradient2"`. */ algorithm?: "gradient2" | "aimd"; /** Sample count for the rolling-min "no-load" RTT. Default 100. */ rttWindow?: number; /** Gradient2 EMA factor in (0,1]; larger reacts faster. Default 0.2. */ smoothing?: number; /** Headroom factor applied to the no-load RTT. Default 2.0. */ tolerance?: number; /** AIMD multiplicative decrease, in `[0.5, 1)`. Default 0.9. */ backoffRatio?: number; /** * **Opt-in Envoy-style forced minRTT recalibration.** The windowed rolling-min no-load baseline * can stay inflated under *sustained* load — every sample in the window carries queuing delay, so * the system never observes a true no-load RTT. When set, the guard periodically *drains* by * clamping its effective ceiling to `probeLimit`, measures the true no-load RTT from the resulting * low-concurrency samples, and adopts it as the fresh baseline. Off by default (today's * Netflix-style windowed min, which only re-baselines if load happens to let up). The probe is * disruptive (throughput dips while it drains), so it runs infrequently. See * `research/bigger-bets/unified/DESIGN.md` §11. */ recalibration?: { /** Re-probe the no-load RTT at most this often (ms). Default `60_000` (Envoy's 60s). Must be > 0. */ intervalMs?: number; /** Effective ceiling to clamp to during a probe so queues drain. Default `minLimit`. Must be ≥ 1. */ probeLimit?: number; /** Clean low-concurrency samples to collect before adopting the fresh baseline. Default 5. Must be ≥ 1. */ probeSamples?: number; }; /** Injectable time source. Default {@link systemClock}. */ clock?: Clock; } /** * A single admission grant from a {@link ConcurrencyGuard}. Returned by every * {@link ConcurrencyGuard.acquire}; the caller must {@link Lease.release} it exactly once when * the work finishes (the second and later calls are no-ops). */ interface Lease { /** `false` means the request is over the inferred ceiling and the caller should shed it (503). */ readonly ok: boolean; /** * Return the slot and record the request's latency. Pass `dropped: true` for a request that * failed/timed out (treated as an overload signal). Idempotent: a second call does nothing. * Safe to call detached (e.g. `const r = lease.release; r()`). */ release(opts?: { dropped?: boolean; }): void; } /** * A dynamically inferred ceiling on in-flight requests. Each completed request feeds its latency * back into the estimate, which rises while the system stays fast and contracts when latency * climbs (queueing) or requests drop. */ interface ConcurrencyGuard { /** Try to take a slot. The returned {@link Lease} is rejected (`ok === false`) when full. */ acquire(): Lease; /** The current inferred ceiling (integer floor of the internal estimate). */ readonly limit: number; /** How many leases are currently outstanding. */ readonly inflight: number; /** A point-in-time snapshot for metrics/introspection. */ stats(): { limit: number; inflight: number; rttNoload: number; lastRtt: number; }; } /** * Construct an adaptive concurrency limiter. The factory keeps the call-site API identical to the * prior implementation; see {@link AdaptiveGuard} for the algorithm and the hot-path rationale. */ declare function adaptiveConcurrency(options?: AdaptiveConcurrencyOptions): ConcurrencyGuard; /** * Zero-dependency fluid-LP solver for two-budget admission (joint-LP policy). * * Solves the deterministic (fluid) relaxation of the revenue-management * admission problem and returns the **bid prices** (LP dual variables) that drive * the joint-LP filter in {@link unifiedAdmission}: * * ``` * max Σ wᵢ vᵢ xᵢ * s.t. Σ wᵢ xᵢ ≤ R (rate budget, dual p_R ≥ 0) * Σ wᵢ cᵢ xᵢ ≤ C (cost budget, dual p_C ≥ 0) * 0 ≤ xᵢ ≤ 1 * ``` * * By LP duality / complementary slackness the optimal admission rule is the * **bid-price test**: admit a request of type `i` iff `vᵢ ≥ p_R + p_C·cᵢ`. * * Strict zero-runtime-deps ⇒ no LP library. We solve it **through the dual**, * which is robust to the degeneracies (equal values, equal costs, density ties) * that defeat naive primal vertex enumeration. The Lagrangian dual of the LP is * * ``` * min D(p_R, p_C) = R·p_R + C·p_C + Σ wᵢ·max(0, vᵢ − p_R − p_C·cᵢ) * s.t. p_R ≥ 0, p_C ≥ 0 * ``` * * `D` is convex and piecewise-linear; its minimum is attained at a **vertex** of * the arrangement of the "bid lines" `vᵢ = p_R + p_C·cᵢ` and the axes. That vertex * set is finite and small — `(0,0)`, each single-axis threshold `(vᵢ, 0)` and * `(0, vᵢ/cᵢ)`, and each pairwise bid-line intersection — so we evaluate `D` at * every candidate and take the minimizer. By strong duality `min D` equals the * primal optimum objective, and the minimizing `(p_R, p_C)` are the bid prices. * Among co-optimal duals (a degenerate tie) we pick the **most selective** one — * the bid prices that reproduce the fluid plan's admit/reject split — matching the * revenue-management convention. The primal admit plan is then recovered as a * feasibility fill consistent with those duals (forced-in/out by reduced-value * sign; the marginal set fills the tight budget(s)). * * Generalizes the 2-type reference in `research/bigger-bets/unified/sim.ts` * (TK-1007) to N types; see THEORY.md and * `research/bigger-bets/joint-lp-admission/DESIGN.md` §3–§4 (D-JLP-7). Correctness * is pinned by `test/admission/fluid-lp.test.ts`: the THEORY fixture, a KKT * certificate, an independent optimality lower bound, AND a tie-heavy * (integer-valued) cross-check against a brute-force oracle — the degenerate class * that the earlier primal-enumeration solver got wrong. * * @packageDocumentation */ /** One request archetype in the workload model handed to {@link solveFluidLp}. */ interface WorkloadType { /** Cost-axis weight per admit (matches `Limiter.check(key, cost)`'s 2nd arg). */ cost: number; /** Business value of admitting one (revenue, priority, …). */ value: number; /** Arrival weight / probability. Need not sum to 1 — used as-is as the per-type usage scale. */ weight: number; /** * OPTIONAL concurrency consumption: the request's expected HOLD (service) time — * how long it occupies a concurrency slot. Required (on every type) iff * {@link FluidLpInput.concBudget} is set, which switches the solver to the 3-budget * (rate + cost + concurrency) mode (TK-1405). Via Little's law an occupancy cap `L` * over a window `T` is the concurrency-seconds budget `L·T` and each admit consumes * `hold`. Ignored in the 2-budget mode. */ hold?: number; } /** Input to {@link solveFluidLp}: the workload mixture plus the budgets. */ interface FluidLpInput { /** The request archetypes. Non-empty. */ types: WorkloadType[]; /** Rate budget R per window (admits/window). > 0. */ rateBudget: number; /** Cost budget C per window (cost units/window). > 0. */ costBudget: number; /** * OPTIONAL concurrency-seconds budget `K = L·T` (concurrency limit × window). When set, * the solver runs in **3-budget mode** (rate + cost + concurrency, TK-1405): every type * must carry a {@link WorkloadType.hold}, and the bid-price test gains a term * `value ≥ p_R + p_C·cost + p_K·hold`. Omit for the classic 2-budget joint-LP. > 0. */ concBudget?: number; } /** Output of {@link solveFluidLp}: the bid prices, the optimal admit plan, and its value. */ interface FluidLpSolution { /** * Bid prices (LP duals). Admit type i iff `value ≥ duals.rate + duals.cost·cost` * (+ `duals.conc·hold` in 3-budget mode). `conc` is present iff the input set * {@link FluidLpInput.concBudget}. */ duals: { rate: number; cost: number; conc?: number; }; /** Optimal admit fraction per input type (same order as `input.types`). */ admitFractions: number[]; /** Optimal objective `Σ wᵢ vᵢ xᵢ` (telemetry / tests). */ objective: number; } /** * Solve the 2-budget fluid LP and return the bid prices, optimal admit plan, and * objective. O(types²) duals + O(types²·2^k) primal fill on the marginal set * (`k = |marginal|`, normally ≤ 2). Zero dependencies. * * @throws ThrottleKitError if `types` is empty, any `cost`/`value`/`weight` is * non-finite or negative, or either budget is non-finite or ≤ 0. * * @experimental Excluded from the 1.x SemVer guarantee (may change in a minor). See STABILITY.md. */ declare function solveFluidLp(input: FluidLpInput): FluidLpSolution; /** * TK-1005 — Lua-fused admission dispatcher. * * The `tk:v1:fused-rc:check` atomic Lua script + the dispatcher that * pumps `EVALSHA → EVAL on NOSCRIPT` against an arbitrary * {@link RedisClientLike}. The script atomically evaluates the rate * axis (GCRA on `KEYS[1]`) and the cost axis (tokenBucket on `KEYS[2]`) * inside one Redis round trip, then combines their results via the * algebra in `combineDecisions` (TK-1002) and returns the combined * Decision plus the per-axis Decisions for `UnifiedAdmitter.lastDecisions`. * * Semantic match to sequential: **each axis writes its own state per its * own admit decision**, independent of the other axis's outcome. So a * rate-admits-but-cost-denies admission still advances rate's TAT — same * as calling `rateLimit({...}).check()` then `rateLimit({...}).check()` * sequentially. The combined Decision still denies. This preserves the * byte-identity claim in DESIGN.md §6 with sequential mode. * * **Atomicity vs sequential.** Both rate and cost transitions run inside * the single Redis EVAL — so concurrent admits cannot interleave between * the rate-write and cost-write. This is the strong atomicity guarantee * fused mode buys over sequential's two-RTT-with-potential-interleave. * * **0.9.0 scope (D-U14).** This file ships the *gcra + tokenBucket* pair * only — the LLM-gateway combination. The constructor enforces strategy * names. Other pairs land as 0.9.x patches when there is demand. */ /** * Atomic fused script. Two keys, two strategy parameter blocks, one * combined return tuple. * * KEYS[1] = rate key (GCRA TAT, SET string `%.17g`) * KEYS[2] = cost key (tokenBucket HASH, fields `t` / `l`) * * ARGV[1] = now (epoch-ms; 0 ⇒ use server TIME — LUA_NOW sentinel) * ARGV[2] = rate.cost (request weight on the rate axis; usually 1) * ARGV[3] = rate.periodMs * ARGV[4] = rate.limit * ARGV[5] = rate.burst * ARGV[6] = cost.cost (cost-axis tokens for this request) * ARGV[7] = cost.capacity * ARGV[8] = cost.refillPerSec * * Returns: a 13-element array of integers * [ allowed, -- 1: combined AND * limit, remaining, resetAt, retryAfterMs, -- 4: combined MIN / MIN / MAX / MAX * rate_allowed, rate_remaining, rate_resetAt, rate_retryAfterMs, -- 4: per-axis rate * cost_allowed, cost_remaining, cost_resetAt, cost_retryAfterMs ] -- 4: per-axis cost * * Per-axis `limit` is omitted from the tuple — the dispatcher fills it * in from the configured `burst` / `capacity` (constant per script * instance, so no point round-tripping it). * * Each axis writes its own state independently of the combined result: * - rate writes its new TAT iff rate_allowed == 1 * - cost writes its new tokens iff cost_allowed == 1 * * This matches the *sequential* mode's per-axis-consume behavior — the * algebra layer's first-deny short-circuit in sequential consumes * earlier axes regardless of downstream denial, and the fused script * intentionally mirrors that (D-U9 in DESIGN.md §14). */ declare const FUSED_GCRA_TOKEN_BUCKET_LUA = "local now = tonumber(ARGV[1])\nif now == 0 then\n local t = redis.call('TIME')\n now = t[1] * 1000 + math.floor(t[2] / 1000)\nend\n-- Physical-TTL floor (ms): only ever EXTENDS a key's PEXPIRE, never shortens it, so it can't change a\n-- decision. Decouples Redis real-time GC from the logical window (see RedisStore.ttlFloorMs). 0 = no-op.\nlocal ttl_floor = tonumber(ARGV[9]) or 0\n-- \u2500\u2500 Rate axis: GCRA on KEYS[1] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlocal rate_cost = tonumber(ARGV[2])\nlocal rate_period = tonumber(ARGV[3])\nlocal rate_limit = tonumber(ARGV[4])\nlocal rate_burst = tonumber(ARGV[5])\nlocal rate_T = rate_period / rate_limit\nlocal rate_tau = rate_T * rate_burst\nlocal rate_inc = rate_T * rate_cost\nlocal rate_tat = tonumber(redis.call('GET', KEYS[1]) or now)\nif rate_tat < now then rate_tat = now end\nlocal rate_new_tat = rate_tat + rate_inc\nlocal rate_allow_at = rate_new_tat - rate_tau\n\nlocal rate_allowed\nlocal rate_remaining\nlocal rate_resetAt\nlocal rate_retryAfterMs\n\nif now < rate_allow_at then\n -- Denied: report what's remaining (against the unchanged TAT) and the wait.\n rate_remaining = math.floor((rate_tau - (rate_tat - now)) / rate_T)\n if rate_remaining < 0 then rate_remaining = 0 end\n rate_allowed = 0\n rate_resetAt = math.ceil(rate_tat)\n rate_retryAfterMs = math.ceil(rate_allow_at - now)\nelse\n -- Allowed: advance the TAT and persist with a PX TTL through the burst window.\n rate_remaining = math.floor((rate_tau - (rate_new_tat - now)) / rate_T)\n if rate_remaining < 0 then rate_remaining = 0 end\n rate_allowed = 1\n rate_resetAt = math.ceil(rate_new_tat)\n rate_retryAfterMs = 0\n local rate_px = math.ceil(rate_new_tat - now)\n if rate_px < 1 then rate_px = 1 end\n if rate_px < ttl_floor then rate_px = ttl_floor end\n redis.call('SET', KEYS[1], string.format('%.17g', rate_new_tat), 'PX', rate_px)\nend\n\n-- \u2500\u2500 Cost axis: tokenBucket on KEYS[2] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlocal cost_cost = tonumber(ARGV[6])\nlocal cost_capacity = tonumber(ARGV[7])\nlocal cost_refill_per_sec = tonumber(ARGV[8])\nlocal cost_refill_per_ms = cost_refill_per_sec / 1000\n\nlocal cost_h = redis.call('HMGET', KEYS[2], 't', 'l')\nlocal cost_tokens = tonumber(cost_h[1])\nlocal cost_last = tonumber(cost_h[2])\nif cost_tokens == nil then cost_tokens = cost_capacity end\nif cost_last == nil then cost_last = now end\nlocal cost_elapsed = now - cost_last\nif cost_elapsed < 0 then cost_elapsed = 0 end\ncost_tokens = cost_tokens + cost_elapsed * cost_refill_per_ms\nif cost_tokens > cost_capacity then cost_tokens = cost_capacity end\nlocal cost_ttl = math.ceil(cost_capacity / cost_refill_per_ms)\nif cost_ttl < 1 then cost_ttl = 1 end\nif cost_ttl < ttl_floor then cost_ttl = ttl_floor end\n\nlocal cost_allowed\nlocal cost_remaining\nlocal cost_resetAt\nlocal cost_retryAfterMs\n\nif cost_tokens >= cost_cost then\n local cost_new_tokens = cost_tokens - cost_cost\n cost_remaining = math.floor(cost_new_tokens)\n if cost_remaining < 0 then cost_remaining = 0 end\n redis.call('HSET', KEYS[2], 't', string.format('%.17g', cost_new_tokens), 'l', string.format('%.17g', now))\n redis.call('PEXPIRE', KEYS[2], cost_ttl)\n cost_allowed = 1\n cost_resetAt = now + math.ceil((cost_capacity - cost_new_tokens) / cost_refill_per_ms)\n cost_retryAfterMs = 0\nelse\n cost_remaining = math.floor(cost_tokens)\n if cost_remaining < 0 then cost_remaining = 0 end\n cost_allowed = 0\n cost_resetAt = now + math.ceil((cost_capacity - cost_tokens) / cost_refill_per_ms)\n cost_retryAfterMs = math.ceil((cost_cost - cost_tokens) / cost_refill_per_ms)\nend\n\n-- \u2500\u2500 Combine via the algebra (combineDecisions in Lua) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlocal allowed\nif rate_allowed == 1 and cost_allowed == 1 then\n allowed = 1\nelse\n allowed = 0\nend\nlocal limit = rate_burst\nif cost_capacity < limit then limit = cost_capacity end\nlocal remaining = rate_remaining\nif cost_remaining < remaining then remaining = cost_remaining end\nlocal resetAt = rate_resetAt\nif cost_resetAt > resetAt then resetAt = cost_resetAt end\nlocal retryAfterMs = rate_retryAfterMs\nif cost_retryAfterMs > retryAfterMs then retryAfterMs = cost_retryAfterMs end\n\nreturn {allowed, limit, remaining, resetAt, retryAfterMs,\n rate_allowed, rate_remaining, rate_resetAt, rate_retryAfterMs,\n cost_allowed, cost_remaining, cost_resetAt, cost_retryAfterMs}"; /** Rate-axis (GCRA) parameter block passed to the fused dispatcher. */ interface FusedRateConfig { /** Strategy discriminator — must be `"gcra"` in 0.9.0 (D-U14). */ strategy: "gcra"; /** Sustained rate: requests per `periodMs`. */ limit: number; /** The period over which `limit` applies, in ms. */ periodMs: number; /** Maximum requests admissible instantaneously (burst). Defaults to `limit`. */ burst?: number; /** Optional key namespace; matches the rate Limiter's prefix. */ prefix?: string; } /** Cost-axis (tokenBucket) parameter block passed to the fused dispatcher. */ interface FusedCostConfig { /** Strategy discriminator — must be `"tokenBucket"` in 0.9.0 (D-U14). */ strategy: "tokenBucket"; /** Bucket capacity: the maximum tokens held, and the largest instantaneous burst. */ capacity: number; /** Sustained refill rate in tokens per second (may be fractional; must be > 0). */ refillPerSec: number; /** Optional key namespace; matches the cost Limiter's prefix. */ prefix?: string; } /** The fused option group on {@link unifiedAdmission} when `backend: "lua-fused"`. */ interface FusedAdmissionOptions { /** The shared Redis client backing both axes (the same client used by the rate / cost limiters). */ client: RedisClientLike; /** Rate-axis config. */ rate: FusedRateConfig; /** Cost-axis config. */ cost: FusedCostConfig; /** * Use the Redis server clock (`TIME`) for `now` inside the script. * Default `true` — protects against node clock skew corrupting state. * Set `false` in deterministic tests that pass an explicit `now` (use * {@link FusedDispatcher.dispatchAt} for that path). */ useServerTime?: boolean; /** * Floor (ms) on the physical key TTL — the fused analog of {@link RedisStoreOptions.ttlFloorMs}. * Default 0 (each axis's logical TTL is used verbatim). Set well above the window with * `useServerTime: false` so a logically-live key isn't reclaimed by Redis real-time GC between writes * (it only EXTENDS the PEXPIRE, so decisions are unchanged — it keeps the fused path byte-identical to * a sequential `RedisStore` configured with the same floor). */ ttlFloorMs?: number; } /** * One combined-plus-per-axis admit outcome from the fused script. Used * to populate the {@link UnifiedAdmitter.lastDecisions} snapshot. */ interface FusedAdmissionResult { /** The combined Decision: AND of allowed, MIN of limit/remaining, MAX of resetAt/retryAfterMs. */ combined: Decision; /** The rate axis's standalone Decision (as if `rateLimit({...}).check()` had run). */ rate: Decision; /** The cost axis's standalone Decision (as if `rateLimit({...}).check()` had run). */ cost: Decision; } /** * Pumps the fused Lua script with EVALSHA → EVAL on NOSCRIPT. One * dispatcher per (client, rate config, cost config); SHA1 is computed * once and cached so the steady-state cost is one EVALSHA per admit. */ declare class FusedDispatcher { #private; constructor(options: FusedAdmissionOptions); /** The per-axis `limit` reported in the per-axis Decisions. */ get rateLimit(): number; /** The per-axis `limit` reported in the per-axis Decisions. */ get costLimit(): number; /** * Run the fused atomic admit for `key` with the given cost-axis weight. * The rate-axis weight is always 1 — the unified admitter's API * doesn't expose a per-axis weight knob. */ dispatch(key: string, costTokens?: number): Promise; /** * Like {@link FusedDispatcher.dispatch} but uses an explicit `now` * (in epoch-ms). Used in tests that pin a deterministic clock — the * Lua's `LUA_NOW` sentinel translates `0` to the server clock, so a * non-zero value pins it instead. */ dispatchAt(key: string, costTokens: number, now: number): Promise; } /** The three axes a unified admission can compose. Used as the key type for {@link UnifiedAdmitter.lastDecisions}. */ type UnifiedAxis = "rate" | "concurrency" | "cost"; /** Options for {@link unifiedAdmission}. Every axis is optional; at least one must be set. */ interface UnifiedAdmissionOptions { /** The rate axis — a {@link Limiter} returning a {@link Decision} for `(key, 1)`. Optional. */ rate?: Limiter; /** The concurrency axis — an {@link ConcurrencyGuard} from `adaptiveConcurrency(...)`. Optional. */ concurrency?: ConcurrencyGuard; /** The cost axis — a {@link Limiter} returning a {@link Decision} for `(key, cost)`. Optional. */ cost?: Limiter; /** * `"sequential"` (default) runs the three axes in turn; first deny short-circuits. * `"lua-fused"` (TK-1005) collapses rate + cost into one Redis EVALSHA — requires * the {@link UnifiedAdmissionOptions.fused} option group; throws if `fused` is missing. * Concurrency stays in-process in either backend (its state is local). */ backend?: "sequential" | "lua-fused"; /** * Required when `backend: "lua-fused"`. Specifies the Redis client and the per-axis * strategy params for the fused atomic script. The `rate` / `cost` Limiters above * are NOT used in fused mode (the script runs the transitions directly against the * Redis client) — pass them anyway only if you want to fall back to sequential at * the call site by re-wrapping; or omit them. * Scope (D-U14): 0.9.0 supports gcra + tokenBucket only; other pairs throw. */ fused?: FusedAdmissionOptions; /** Injectable time source. Defaults to {@link systemClock}; forwarded to the lease shim. */ clock?: Clock; /** * Admission policy. `"marginal"` (DEFAULT) = the 0.9.0 marginal-AND behavior * (each axis allows independently). `"joint-lp"` = additionally apply a * bid-price filter (admit iff `value ≥ p_R + p_C·cost`) on top of marginal * feasibility — opt-in, research-backed (research/bigger-bets/joint-lp-admission). * Strictly more selective than `"marginal"`: it only ever *removes* admits, so * it cannot break any existing limit/safety property (D-JLP-5). */ policy?: "marginal" | "joint-lp"; /** * Required iff `policy: "joint-lp"`. Supply EXACTLY ONE of: * - `duals`: precomputed bid prices (you solved the LP elsewhere); or * - `workload`: a model the library solves once, at construction, via {@link solveFluidLp}. * Requires a `cost` axis (the bid-price test is over the cost budget; D-JLP-11). */ jointLp?: { duals?: { rate: number; cost: number; conc?: number; }; workload?: FluidLpInput; /** * Opt-in **online dual refinement** (D-JLP-8; Devanur–Hayes "sample-then-price"). * REQUIRES the `workload` form (not bare `duals`): the model is the only input that * carries both the construction **prior** AND the per-arrival budget normalization * (`workload.rateBudget` / `workload.costBudget`) that the online re-solve and the * on-sample self-test both need. * * During the first `sampleWindow` policy-evaluated requests the filter prices with * the prior while tallying the observed `(cost, value)` type mixture. At the window * boundary it re-solves the fluid LP from what it actually saw and adopts the learned * bid prices **only if they strictly beat the prior on the buffered sample** (replayed * under the window-scaled budget `rateBudget·W` / `costBudget·W`), else keeps the * prior — then freezes for the lifetime of this admitter. * * Self-validating (the load-bearing property): it is **never worse than the static * prior on the observed sample**, yet **escapes a misspecified prior** — a * catastrophically wrong prior that would admit nothing is rescued. Until the window * fills, behavior is byte-identical to static joint-LP with `workload`. * * **Scope of the guarantee.** Non-inferiority holds on the *observed sample* only; it does * NOT imply full-horizon dominance. Under non-stationary / autocorrelated arrivals the * window can be unrepresentative, so an adopted dual may do *slightly* worse over the full * stream (the autocorrelation cousin of the ρ=+1 foil — bounded and small in practice, and * still far better than no policy). With a `concurrency` axis configured, "policy-evaluated" * counts requests that PASSED concurrency (the bid filter sits after it), so the window * reflects the post-concurrency mixture. See * `research/bigger-bets/joint-lp-admission/DESIGN.md` §6 + the gate (`adaptive-gate.ts`). */ adaptive?: { sampleWindow: number; }; }; } /** Per-call options to {@link UnifiedAdmitter.admit} / {@link UnifiedAdmitter.admitSync}. */ interface UnifiedAdmitOptions { /** * Key passed to the rate / cost axes. Concurrency is keyless. Defaults to the empty string * (interpreted as the "global" rate / cost bucket by the underlying limiters). */ key?: string; /** * Cost weight passed to the cost axis (matches `Limiter.check(key, cost)`'s second arg). * Defaults to 1. */ cost?: number; /** * The request's value `vᵢ` for the joint-LP bid-price test. Ignored unless * `policy: "joint-lp"`. Defaults to 1 (D-JLP-10) — a workload that doesn't set * per-request value collapses joint-LP to a cost-threshold filter. */ value?: number; /** * The request's expected HOLD (service) time for the **3-axis** joint-LP concurrency * term (TK-1405): the bid test becomes `value ≥ p_R + p_C·cost + p_K·hold`. Ignored * unless `policy: "joint-lp"` with a concurrency budget configured (`jointLp.workload` * with `concBudget`, or `jointLp.duals.conc`). Must be in the SAME units as the workload * model's `hold`. **Defaults to 0**, and a missing, non-finite (`NaN`/`Infinity`), or negative * `hold` all contribute NO concurrency term (fail-open: the concurrency price only ever * *rejects* when you give it a positive finite hold — a bad estimate never wrongly rejects, and * a hog cannot dodge the price by reporting a negative hold). A 3-axis admitter with `hold` * omitted therefore behaves exactly like 2-axis. */ hold?: number; } /** The result of one admit call. `release` is the lifecycle hook for the concurrency slot (or a no-op when denied). */ interface UnifiedAdmission { /** The combined Decision across all configured axes (per {@link combineDecisions}). */ decision: Decision; /** * Release the held concurrency slot when the work finishes. Pass `dropped: true` * to signal an overload (timeout / error) — the adaptive concurrency limit * contracts on a drop. On a denied admission this is a no-op (any lease that * was transiently acquired has already been released as part of the short-circuit). * Idempotent: second and later calls do nothing. */ release(opts?: { dropped?: boolean; }): void; /** * The axis whose denial bound this admission (`"rate"` / `"concurrency"` / `"cost"`), or `undefined` * when admitted, or when the joint-LP policy filter bound (see {@link UnifiedAdmission.policyDenied}). * The ergonomic form of {@link UnifiedAdmitter.lastDecisions} for the common "why was this denied?" * case; equal to the OTel `throttlekit.binding_axis` attribute. Append-only-optional (SemVer). */ readonly bindingAxis?: UnifiedAxis; /** * True iff this admission was denied specifically by the joint-LP bid-price * filter (every per-axis budget had slack, but `value < p_R + p_C·cost`). * Absent/falsy under `policy: "marginal"` or any axis-bound denial. Lets the * TK-1008 OTel `throttlekit.binding_axis` attribute report `"policy"`. */ policyDenied?: boolean; } /** * A constructed unified admitter — see {@link unifiedAdmission}. Two entry points * (matching the existing {@link Limiter.check} / {@link Limiter.checkSync} pattern): * * - {@link UnifiedAdmitter.admit} is async; works for any backend mix. * - {@link UnifiedAdmitter.admitSync} is sync; throws when any configured axis * lacks a synchronous code path (i.e. a Redis-backed limiter without * `applySync`). * * Plus per-axis introspection via {@link UnifiedAdmitter.lastDecisions} (used by * TK-1008's `throttlekit.binding_axis` OTel attribute). */ interface UnifiedAdmitter { admit(opts?: UnifiedAdmitOptions): Promise; admitSync(opts?: UnifiedAdmitOptions): UnifiedAdmission; /** * Snapshot of the most recent admit's per-axis decisions. Unconfigured axes are * `undefined`; short-circuit decisions also leave downstream axes `undefined` * (so the caller can see *which* axis bound). Each call returns a fresh frozen * object — safe to leak into telemetry. */ lastDecisions(): Readonly>>; } /** * Compose three orthogonal admission axes (rate / concurrency / cost) into one * {@link UnifiedAdmission} via the algebra in {@link combineDecisions}. See * `research/bigger-bets/unified/DESIGN.md` §4.2 for the locked API and §4.2.2 * for the sequential evaluation order: **concurrency first** (in-process; cheapest * fail), then **rate**, then **cost**; first deny short-circuits and releases any * concurrency slot transiently acquired upstream of the binding axis. * * Commutativity of `combineDecisions` (TK-1002) guarantees this ordering doesn't * change the *result* — only the short-circuit cost. The result of every admit * call is the field-by-field MIN / MAX / AND of the per-axis decisions, so a * client sees the binding axis's `limit` / `remaining` and the dominant * `retryAfterMs` / `resetAt` regardless of which axis denied. * * **Lease lifecycle.** When the concurrency axis admits but a later axis denies, * the held slot is released immediately (with `dropped: false` — the deny is * upstream, not an overload signal). The caller's returned `release` is then a * no-op (the slot is already free). On a triple-success admit, `release` is wired * to the underlying lease's `release`; the caller MUST call it once when the * work finishes (or always-call from a `finally` block — release is idempotent). * * **`backend: "lua-fused"`** (TK-1005) collapses rate + cost into one Redis * EVALSHA via the {@link UnifiedAdmissionOptions.fused} option group; concurrency * stays in-process. Sequential is the universal default. */ declare function unifiedAdmission(options: UnifiedAdmissionOptions): UnifiedAdmitter; export { type AdaptiveConcurrencyOptions as A, type ConcurrencyGuard as C, FUSED_GCRA_TOKEN_BUCKET_LUA as F, type Lease as L, type UnifiedAdmission as U, type WorkloadType as W, type FluidLpInput as a, type FluidLpSolution as b, type FusedAdmissionOptions as c, type FusedAdmissionResult as d, type FusedCostConfig as e, FusedDispatcher as f, type FusedRateConfig as g, type UnifiedAdmissionOptions as h, type UnifiedAdmitOptions as i, type UnifiedAdmitter as j, type UnifiedAxis as k, adaptiveConcurrency as l, solveFluidLp as s, unifiedAdmission as u };