/** * Self-pacing against a STATED rate limit — the relay holding its OWN request rate under a * ceiling a deployment told it about, BEFORE the next 429 (owner direction 2026-09-10: *"The relay * should be tracking requests from all IDEs on the machine, anything that runs through the relay, * so it can use rate-limited messages to calculate when it might need to slow something down."*). * * The third demotion term beside `quota-demotion.ts` and `latency-demotion.ts`, same shape — a * pure resolver, a `create…Fn` factory that can never throw into the request path, and a label — * folded into `targetUsability` as a one-way band. It answers a DIFFERENT question from quota * demotion, stated so the two are not read as one policy in two homes: * * - `quota-demotion.ts` is the ALLOWANCE view: a bucket whose `remaining` (stated, or a stated * limit minus the ledger's usage in the current UTC-aligned period) is spent joins `cooling` * until the reset the evidence stated. * - this module is the RATE view: how many attempts THIS relay itself started against the cell * in the trailing window of the stated period, held against the stated ceiling. It reads the * breaker's per-cell attempt-start log (`CircuitBreaker.attemptsInWindow`), which every egress on * both fronts passes through, so every client on the machine that routes through the relay is * counted — a Codex session and a Claude Code session share one cell's window. * * ⚠ A SLIDING window, deliberately: a count that never exceeds L in any trailing window cannot * exceed L in a provider's fixed window either, whichever alignment the provider uses — and the * relay is never told. The ledger's UTC-minute cell (what quota demotion reads) lets a burst at a * boundary run 2L in 61 seconds; this does not. * * ⚠ Three ceilings pace, ranked by `resolveLimit` (`availability.ts`): the provider's own header * `limit`, the operator's `limits` block, and a LEARNED `rate-limit-*` fact parsed from a 429 body * (`rate-limits.ts`). **The learned rung paces WITHOUT the `routing.quota.enforceLearned` opt-in.** * That is the owner's 2026-09-10 direction and the backlog property — "a 429 that states a window * updates that pacing without a human verdict" — and it is what makes the loop close live: the * request path records the fact at the 429, and the next `targetUsability` reads it. The M2 gate * inside `quota-demotion.ts` is untouched; it governs the allowance path, which can register a * breaker cooldown, while this term registers nothing and re-resolves per request. A `published` * (catalogue) figure never paces — `PACES_ON_LIMIT_BASIS` closes that decision. * * ⚠ **A limit nobody stated has NO EFFECT.** No bucket ⇒ null. A window the log cannot answer for * (a token sum with an unknown member, a saturated log below the ceiling) ⇒ null. There is no * tunable margin: the relay steps aside exactly at the stated figure, because "approaching" would * need a number nobody stated. Only `minute` and `day` periods are paced — the two a stated rate * limit names; `month` stays the allowance path's, and `unknown` has no window to count. * * ⚠ **Demote only; never drop, never refuse, never delay.** A paced cell joins the `paced` band * behind `live` and `slow` and ahead of the failure bands (`candidate-runner.ts`), so the next * request goes to another candidate while this one's window drains — and with no other candidate * it is still walked. Refusing on a count is the hard cap's business (`hard-cap.ts`), and only an * OPERATOR-declared figure may do that. Announced as `x-llm-relay-paced` when the walk's first * choice was displaced by it. * * ⚠ Stated residue: the count is the relay's own attempts. A probe (`ping/ping.ts`, one token) and * any traffic from outside the relay do not enter it, so the count is a floor of what the provider * meters — the fail-safe direction (less pacing), and the provider's 429 still teaches the cell. * Token windows sum the request's INPUT estimate only, for the same reason. */ import type { Config } from "./config.js"; import type { PacingConfig } from "./config-types.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 LimitProvenance } from "./availability.js"; /** The two periods a stated rate limit names; `month` and `unknown` are never paced. */ export type PacingPeriod = Extract; /** The trailing window each paced period reads — a total table, so a new period is a compile error. */ export declare const PACING_WINDOW_MS: { readonly minute: 60000; readonly day: 86400000; }; /** * Which limit provenances may PACE. ⚠ A total table closed with `satisfies`, not an `if` on the * two names it happened to know: `resolveLimit`'s basis union carries `published`, and an * unconditional fall-through would have paced on a catalogue figure — the closed-vocabulary * defect `CLAUDE.md` records eight times, falling to the STRONGER claim. */ declare const PACES_ON_LIMIT_BASIS: { readonly "provider-stated": true; readonly configured: true; readonly learned: true; readonly published: false; }; /** The basis a verdict may carry: exactly the provenances `PACES_ON_LIMIT_BASIS` admits. */ export type PacingLimitBasis = { [K in LimitProvenance]: (typeof PACES_ON_LIMIT_BASIS)[K] extends true ? K : never; }[LimitProvenance]; /** One pacing verdict — the smallest honest statement of "why this cell steps aside for now". */ export interface PacingVerdict { readonly axis: QuotaAxis; readonly period: PacingPeriod; /** The stated ceiling. */ readonly limit: number; /** Who stated it. */ readonly limitBasis: PacingLimitBasis; /** Attempts this relay started (or their estimated INPUT tokens) inside the trailing window. ≥ `limit`. */ readonly counted: number; } /** The per-request evaluator threaded through the walk-order helpers. Null ⇒ no opinion. */ export type PacingFn = (attempt: ResolvedAttempt, now: number) => PacingVerdict | null; export interface PacingDeps { readonly cfg: Config; /** The breaker, narrowed to the two reads this term makes — it never writes to it. */ readonly breaker: Pick; /** * ⚠ The SHAPE is owned by `config-types.ts` (`PacingConfig`) and imported, never re-declared — * the `latency-demotion.ts` rule. `config/routing-parser.ts` normalizes the boolean shorthand * away, so this module never decides what `false` means. */ readonly settings?: PacingConfig | undefined; } /** Resolve the knobs once. Absent, or an empty object, means the default — which is ON. */ export declare function resolvePacingSettings(settings: PacingConfig | undefined): { enabled: boolean; }; /** The pacing verdict for ONE credential×deployment cell, from whatever ceilings were stated. */ export declare function resolvePacing(deps: PacingDeps, attempt: ResolvedAttempt, now: number): PacingVerdict | null; /** * 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-pacing behaviour — rather * than to a refused request. Deliberately silent: routing hints are not log-worthy events. */ export declare function createPacingFn(deps: PacingDeps): PacingFn; /** * `" (requests/minute 60 of 60 in the trailing minute, learned)"` — bounded, metadata only: * a spec, two counts and a provenance, never a prompt, a credential or an id. A token bucket says * so, because its count is an INPUT estimate and not what the provider metered. */ export declare function pacingLabel(spec: string, verdict: PacingVerdict): string; export {};