/** * Quota as a DEMOTION term — spec §5.4 "Enforcement (routing input)", reconciliation Gap 12. * * The §5.1 ladder (`availability.ts`) already resolves `remaining` per (scope, axis, period); this * module decides what routing DOES with it, and the answer is deliberately narrow: when a * trustworthy figure says a cell is spent, the cell joins the COOLING band of the walk order — * demoted behind live members, never dropped, expiring at the very `resetsAt` the evidence * stated. Three gates keep it from becoming a guess-driven throttle: * * - `provider-stated`, `derived:provider-stated` and `derived:configured` gate by default * (decision M1: on by default). `derived:provider-stated` is a STALE observation's stated * limit minus this period's locally measured usage — the limit was first-party, only the * subtraction is ours, so it is first-party enough to gate (same rule as * `availability.ts` `routingEligible`). * - `derived:learned` gates ONLY under `routing.quota.enforceLearned` (decision M2): the limit * came from a regex over vendor prose, and a mis-parsed axis would throttle a healthy * deployment on a number nobody stated. It stays display-only until the operator opts in. * `derived:published` (a catalogue figure) NEVER gates. * - unknown ⇒ NO EFFECT WHATSOEVER — same rule as the context guardrail. A bucket with neither * an eligible observation nor a resolvable limit-plus-usage produces no opinion here. * * Pure and bounded: no IO, no clock of its own, and a handful of (axis, period) pairs per cell. * Every ledger read it may make (`usedInWindow`) is the store's IN-MEMORY window read, memoized * to at most one read per period. The * factory wraps everything in try/catch — this runs on the request path, and a routing hint must * never be able to fail a request. It logs nothing: routing is not an error stream. */ import type { Config } from "./config.js"; import type { CircuitBreaker } from "./circuit-breaker.js"; import type { ResolvedAttempt } from "./resolved-attempt.js"; import type { QuotaAxis, QuotaPeriod } from "./quota-observation.js"; import { type RemainingResolution, type ResetsAtResolution } from "./availability.js"; /** One spent, gateable bucket — the smallest honest statement of "why this cell steps aside". */ export interface QuotaDemotion { readonly axis: QuotaAxis; /** * MAY be `"unknown"`, and the reason the old `Exclude` was wrong is worth keeping: it read * "a bucket without a known period cannot reach a boundary to expire at", which the wire * falsifies. Groq answers `x-ratelimit-limit-requests` with limit, remaining AND reset, and the * reset is what a demotion expires against — rung 1 of `resolveResetsAt` reads it directly and * never needs a period. An unknown-period bucket is admitted only when the provider stated that * reset (`collectQuotaBuckets`), so this member can only ever carry a first-party expiry. What * an unknown period still cannot do is rung-2 arithmetic; `localUsedForPeriod` enforces that. */ readonly period: QuotaPeriod; /** ≤ 0 by construction; preserved rather than clamped — overshoot is information. */ readonly remaining: number; /** `provider-stated` | `derived:provider-stated` | `derived:configured` | `derived:learned` — never a guess label. */ readonly basis: RemainingResolution["basis"]; /** When the band lifts; non-null by construction (see resolveQuotaDemotion). */ readonly resetsAt: number; /** * Which §5.2 rung produced `resetsAt`. Imported from `ResetsAtResolution` so the spelling has * ONE home, but `Exclude`d of null: `resolveQuotaDemotion` skips a bucket whose reset or basis * is null (no invented duration), so a demotion that exists always knows its rung. Importing * the bare union would hand every consumer a null case that cannot occur. */ readonly resetsAtBasis: Exclude; } /** The per-request evaluator threaded through the walk-order helpers. Null ⇒ no demotion. */ export type QuotaDemotionFn = (attempt: ResolvedAttempt, now: number) => QuotaDemotion | null; export interface QuotaDemotionDeps { readonly cfg: Config; readonly breaker: CircuitBreaker; /** * The accounting store, narrowed to its in-memory `usedInWindow` read — the SAME seam packet * D's dashboard producer takes. Absent (a bare programmatic proxy, or no store handed to the * server) ⇒ `localUsed` is null ⇒ every derived rung produces null ⇒ the term has no effect. * Never a disk read. */ readonly accounting?: Pick | null; } export declare function bucketRank(axis: QuotaAxis, period: QuotaPeriod): number; /** * Build the request-path evaluator. The wrapper is the safety seam: NOTHING inside may throw into * the request path, and a failure degrades to "no opinion" — the pre-Gap-12 behaviour — rather * than to a refused request. Deliberately silent: routing hints are not log-worthy events. */ export declare function createQuotaDemotionFn(deps: QuotaDemotionDeps): QuotaDemotionFn; /** `" (requests/minute remaining 0, provider-stated)"` — bounded, metadata only. */ export declare function quotaDemotionLabel(spec: string, demotion: QuotaDemotion): string;