import { FabricError } from './error.js'; /** * Per-process / per-session USD ceilings on model spend. Builds on the static * price table populated by `applyEstimatedCost` in every provider's parse path. * * Limits are evaluated **after** each call's cost lands on `usage.costUsd`. * Pre-call estimation is intentionally not done here — accurate token counts * aren't known until the response arrives, and over-eager pre-call refusal * makes the loop unpredictable for users with bursty workloads. * * `onExceed: 'throw'` (default) halts the loop with `CostLimitExceededError`. * `onExceed: 'approve'` emits an `approval_requested` event with * `kind: 'cost-limit'` and pauses the session until an approver responds. * * Budgets are per-session — a forked / replayed session starts with a fresh * budget. Cross-process aggregation requires an external store (out of scope * for v1.4). */ /** * Async store for cross-process spend aggregation. Pair with `CostLimit.scopeKey` * + `CostLimit.perScope` to enforce a budget that survives process restarts — * "tenant:acme spends ≤ $50 today" or "company-wide ≤ $100 this hour". * * Built-in implementations: * - `inMemoryCostBudgetStore()` — process-local; default. * - `@fabric-harness/node` exposes `postgresCostBudgetStore({ pool })`. */ export interface CostBudgetStore { /** Increment cumulative spend for `scope` and return the new total. */ incrementUsd(scope: string, deltaUsd: number): Promise; /** Read current total without mutating. */ getUsd(scope: string): Promise; /** Atomically reserve spend only when the resulting total does not exceed `limitUsd`. */ reserveUsd?(scope: string, deltaUsd: number, limitUsd: number): Promise<{ reserved: boolean; totalUsd: number; }>; /** Reset (used by tests / period rollover). Optional. */ reset?(scope: string): Promise; } /** * Sugar over `CostLimit.perScope + scopeKey + store` for the common * "per-tenant ceiling per period" pattern. Pick one of `perDayUsd`, * `perHourUsd`, or `perMonthUsd`; when multiple are set, the most * restrictive (smallest absolute) wins. * * Scope key convention: `tenant::` where `` is * `day:YYYY-MM-DD`, `hour:YYYY-MM-DDTHH:00Z`, or `month:YYYY-MM`. Reset * semantics (rollover) are the host's job — call `store.reset(scopeKey)` * from a scheduled task to clear the period total. * * ```ts * import { tenantCostLimit, inMemoryCostBudgetStore } from '@fabric-harness/sdk'; * * const fabric = await init({ * tenantId, * costLimit: tenantCostLimit(tenantId, { perDayUsd: 50 }), * }); * ``` */ export declare function tenantCostLimit(tenantId: string, options: TenantCostLimit): CostLimit; export interface TenantCostLimit { /** Hourly ceiling in USD. */ perHourUsd?: number; /** Daily ceiling in USD. */ perDayUsd?: number; /** Monthly ceiling (calendar UTC). */ perMonthUsd?: number; /** Backing store. Defaults to in-memory; pair with Postgres for production. */ store?: CostBudgetStore; onExceed?: 'throw' | 'approve'; /** Test seam — defaults to `new Date()`. */ now?: Date; } /** Process-local cost budget store. Default when `store` is not provided. */ export declare function inMemoryCostBudgetStore(): CostBudgetStore; export interface CostLimit { /** Hard ceiling per individual model call (USD). */ perCall?: number; /** Hard ceiling for the entire session (USD). */ perSession?: number; /** * External-scope ceiling. Pair with `scopeKey` and `store` to enforce a * budget across processes. Useful for per-tenant / per-day caps. */ perScope?: number; /** * Opaque key naming the scope (e.g. `'tenant:acme'`, `'day:2026-05-08'`, * `'company-wide'`). Required when `perScope` is set. fabric-harness * never interprets this key — the store does. */ scopeKey?: string; /** Cross-process store backing `perScope`. Defaults to in-memory. */ store?: CostBudgetStore; /** * Behavior when a limit is exceeded. * - 'throw' (default): throw immediately, halting the loop. * - 'approve': emit an approval_requested event with `kind: 'cost-limit'` * and pause until approved. Re-throw on denial. */ onExceed?: 'throw' | 'approve'; /** * Whether the scope is tracked incrementally in-process or read from an * external source. Defaults to `'incremental'`. */ scopeSource?: 'incremental' | 'external'; /** * External source for actual (non-estimated) spend. When provided, the * budget tracker can query real-time cost data from the provider. */ actualSource?: ActualCostSource; /** * TTL for caching actual cost lookups (milliseconds). When omitted, the * actual source's default or per-query option is used. */ actualsCacheTtlMs?: number; } export interface CostLimitContext { scope: 'call' | 'session' | 'cross-process'; observedUsd: number; limitUsd: number; /** External scope key, populated for `scope: 'cross-process'`. */ scopeKey?: string; /** ISO timestamp when actual cost data was last fetched from the provider. */ actualsFetchedAt?: string; /** TTL (in ms) for the cached actual cost data used in this observation. */ cacheTtlMs?: number; } /** * Attribution dimensions for a single cost observation. All fields are * optional so callers can tag as much or as little metadata as they have. */ export interface CostAttribution { agentId?: string; userId?: string; tenantId?: string; model?: string; provider?: string; sessionId?: string; turnId?: string; [key: string]: string | undefined; } /** * Query filters for retrieving aggregated cost attribution rows. */ export interface AttributionQuery { agentId?: string; userId?: string; tenantId?: string; model?: string; provider?: string; /** ISO date string (inclusive). */ startDate?: string; /** ISO date string (inclusive). */ endDate?: string; /** Dimensions to group by. Defaults to all if omitted. */ groupBy?: Array; } /** * A single row of aggregated cost attribution data. */ export interface CostAttributionRow { agentId?: string; userId?: string; tenantId?: string; model?: string; provider?: string; /** Period identifier, e.g. `day:2026-06-23`. */ period: string; /** Estimated cost from the static price table. */ estimatedUsd: number; /** Actual cost from the provider's billing API. */ actualUsd: number; /** Actual DBU (Databricks Unit) spend, if applicable. */ actualDbus: number; /** Difference between estimated and actual cost. */ deltaUsd: number; /** Number of calls in this aggregation. */ calls: number; } /** * External source for actual (non-estimated) spend. Implementations can * query provider billing APIs, Databricks usage tables, or other real-time * cost data. */ export interface ActualCostSource { /** * Fetch the actual USD spend for a scope, optionally filtered by * attribution dimensions. */ getUsd(scopeKey: string, attribution?: CostAttribution, options?: { cacheTtlMs?: number; }): Promise<{ usd: number; dbus?: number; actualsFetchedAt?: string; cacheTtlMs?: number; }>; /** * Query aggregated cost attribution rows for reporting or debugging. */ queryAttribution(filters: AttributionQuery): Promise; } export declare class CostLimitExceededError extends FabricError { readonly context: CostLimitContext; constructor(context: CostLimitContext); } /** * Tracks cumulative session spend. Cheap to construct; one per session. */ export declare class CostBudgetTracker { private readonly limit; private sessionTotalUsd; private pendingScopeViolation; private defaultStore; constructor(limit: CostLimit | undefined); /** How `perScope` is enforced: `'incremental'` (estimate counter) or `'external'` (actual-cost source). */ get scopeSource(): 'incremental' | 'external'; /** Has any budget configured. */ enabled(): boolean; /** * Record this call's cost and return a per-call/per-session violation if * any local limit was crossed. Returns undefined when within local budget. * The cross-process check runs separately via {@link observeScopeAsync}. */ observe(callUsd: number): CostLimitContext | undefined; /** * Cross-process scope check. Returns a violation when the scope's running * total exceeds `perScope` after incrementing by `callUsd`. No-op when * `perScope`/`scopeKey` are not configured. * * For `scopeSource === 'incremental'` (default), increments the store and * compares. For `scopeSource === 'external'`, queries `actualSource` instead. */ observeScopeAsync(callUsd: number, attribution?: CostAttribution): Promise; /** Store a pending scope violation for asynchronous reconciliation. */ setPendingScopeViolation(violation: CostLimitContext): void; /** Take and clear the pending scope violation, if any. */ takePendingScopeViolation(): CostLimitContext | undefined; /** Total spend so far this session. */ total(): number; onExceed(): 'throw' | 'approve'; } //# sourceMappingURL=cost-budget.d.ts.map