import { type RateLimitStore } from '../ops/rate-limit-adapters.js'; import type { TenantGate } from '../types/server.js'; import type { BudgetPeriod, BudgetReservationRecord } from '../types/lifecycle.js'; import { type BudgetReservationChange } from '../lifecycle/reservations.js'; export { type TenantLimit, TenantLimitError } from './errors.js'; export type { TenantGate } from '../types/server.js'; export type { BudgetPeriod } from '../types/lifecycle.js'; /** What one tenant may do. Every limit is optional; one that is not set is not enforced. */ export interface TenantLimits { /** Runs the tenant may have accepted and unfinished at once, queued ones included. */ maxActiveRuns?: number; /** Runs the tenant may start per window, such as `{ runs: 100, windowMs: 60_000 }`. */ rate?: { runs: number; windowMs: number; }; /** * What the tenant may spend per period, in US dollars, from what its runs record through * `recordCost()`. A tenant at its budget gets no new runs until the period resets. `stopRuns` * also cancels runs in flight the moment the budget is spent. */ budget?: { usd: number; period?: BudgetPeriod; stopRuns?: boolean; }; } /** Options for `tenantLimits()`. */ export interface TenantLimitsOptions { /** Limits for any tenant without its own, and for requests without a tenant. */ default?: TenantLimits; /** * Limits per tenant id, or a function that looks them up — from a plan, a database, or a header. * Returning nothing applies `default`. A function is called once per admitted run, so cache * anything slow. */ tenants?: Record | ((tenantId: string | undefined) => TenantLimits | undefined | Promise); /** Where active runs and spending are counted. Defaults to memory; `RedisTenantUsage` shares them. */ usage?: TenantUsageStore; /** Where rate windows are counted. Defaults to memory; `RedisRateLimitStore` shares them. */ rates?: RateLimitStore; /** * How long a run's active slot lasts if its release never comes, as when its worker dies, in * milliseconds. Set it above the longest run. Defaults to an hour. */ slotTtlMs?: number; /** Replaces the system clock, for tests. The default usage and rate stores keep time by it too. */ now?: () => Date; } /** * Where tenant usage is counted: active-run slots that lapse on their own, and spending totals that * reset each period. Five operations, so any shared store can implement it. */ export interface TenantUsageStore { /** Takes a slot for `member` under `key` unless `max` are held, and resolves true when it did. Holding one already counts as taken. */ acquire(key: string, member: string, max: number, ttlMs: number): Promise | boolean; /** Frees a slot. Freeing one that is not held does nothing. */ release(key: string, member: string): Promise | void; /** Slots held under a key. */ count(key: string): Promise | number; /** Adds to a total that resets at `resetAt`, epoch milliseconds, and resolves to the new total. */ add(key: string, amount: number, resetAt: number): Promise | number; /** A total, or 0. */ total(key: string): Promise | number; } /** One tenant's limits and what it has used, from `GET /usage` or `report()`. */ export interface TenantUsage { /** The tenant, or nothing for requests without one. */ tenantId?: string; /** The limits that apply to it. */ limits: TenantLimits; /** Runs accepted and unfinished now. */ activeRuns: number; /** Spending in the current period. */ spend: { usd: number; period: BudgetPeriod; resetsAt: string; }; } /** The gate `tenantLimits()` returns: the server's `TenantGate`, plus reports for people. */ export interface TenantLimiter extends TenantGate { /** A tenant's limits and usage. */ usage(tenantId?: string): Promise; /** Usage for these tenants, or for every tenant named in `tenants` when it is a record. */ report(tenantIds?: readonly string[]): Promise; } /** * Per-tenant limits the agent server enforces: active runs, runs per window, and spending per * period. Give the result to `createAgentServer({ tenants })`. * * A run is admitted when it is accepted: the budget is checked, the rate window counts it, and it * takes an active slot, in that order. A refusal is a `429` with `Retry-After` when the wait is known. * The slot is released when the run ends, on whichever worker ran it; the rate and budget counters * move on their own. Point `usage` and `rates` at Redis and every replica enforces one set of limits. */ export declare function tenantLimits(options: TenantLimitsOptions): TenantLimiter; /** * Tenant usage in process memory, and the budget reservations against its totals. The default, and * enough for one replica. */ export declare class MemoryTenantUsage implements TenantUsageStore { private readonly now; private readonly slots; private readonly totals; private readonly book; constructor(now?: () => number); /** Takes a slot unless `max` are held. */ acquire(key: string, member: string, max: number, ttlMs: number): boolean; /** Frees a slot. */ release(key: string, member: string): void; /** Slots held under a key. */ count(key: string): number; /** Adds to a total that resets at `resetAt`. */ add(key: string, amount: number, resetAt: number): number; /** A total, or 0 once it has reset. */ total(key: string): number; /** Records a budget reservation and adds it to its total. */ hold(record: BudgetReservationRecord): number; /** Moves a budget reservation and its total together. */ settle(id: string, change: BudgetReservationChange): boolean; /** Extends a reserved budget reservation's lease. */ renew(id: string, leasedUntil: number): boolean; /** Reserved budget reservations whose lease lapsed. */ lapsed(now: number, limit: number, operation?: string): BudgetReservationRecord[]; /** One budget reservation. */ reservation(id: string): BudgetReservationRecord | undefined; private live; } /** * The Redis commands the tenant usage store needs. `eval` runs each step as one atomic script, so * two replicas admitting runs for one tenant at the same moment cannot both take its last slot. */ export interface RedisTenantUsageLikeClient { /** Runs a Lua script, ioredis style: the script, the number of keys, then keys and arguments. */ eval(script: string, numKeys: number, ...args: string[]): Promise; } /** Options for `RedisTenantUsage`. */ export interface RedisTenantUsageOptions { /** Prefix for every key. Defaults to `nexus-ai-pro:tenants:`. */ prefix?: string; } /** * Tenant usage in Redis, shared by every replica: active slots in sorted sets scored by when they * lapse, and spending in counters that expire when their period ends. */ export declare class RedisTenantUsage implements TenantUsageStore { private readonly client; private readonly prefix; constructor(client: RedisTenantUsageLikeClient, options?: RedisTenantUsageOptions); /** Takes a slot unless `max` are held, atomically. */ acquire(key: string, member: string, max: number, ttlMs: number): Promise; /** Frees a slot. */ release(key: string, member: string): Promise; /** Slots held under a key, dropping lapsed ones first. */ count(key: string): Promise; /** Adds to a total that expires at `resetAt`. */ add(key: string, amount: number, resetAt: number): Promise; /** A total, or 0. */ total(key: string): Promise; /** Records a budget reservation and adds it to its total, in one script. */ hold(record: BudgetReservationRecord): Promise; /** Moves a budget reservation and its total together, in one script, if it stands where `change.from` allows. */ settle(id: string, change: BudgetReservationChange): Promise; /** Extends a reserved budget reservation's lease. */ renew(id: string, leasedUntil: number): Promise; /** Reserved budget reservations whose lease lapsed by `now`, soonest lapsed first. */ lapsed(now: number, limit: number, operation?: string): Promise; /** One budget reservation, while it is kept. */ reservation(id: string): Promise; private registry; private index; private unindex; private totalOf; /** A record as it was held, with what has changed since laid over it. */ private read; }