import type { AiMiddleware, MiddlewareContext } from '../types.js'; import { type ModelPriceEntry } from './pricing.js'; import type { BudgetPeriod, BudgetStorage } from './storage.js'; /** * Per-period caps in USD. Either or both can be set; periods with `null` * / `undefined` are not enforced. */ export interface BudgetCaps { daily?: number | null; monthly?: number | null; } export interface BudgetExceededArgs { userId: string; period: BudgetPeriod; /** Spend recorded BEFORE the rejected debit. */ spent: number; cap: number; ctx: MiddlewareContext; } export interface WithBudgetOptions { /** * Resolves the user identifier for a given request. Return `null` (or * `undefined`) to bypass budget enforcement entirely — useful for * unauthenticated paths or admin tooling. * * Async return supported. */ user(ctx: MiddlewareContext): string | null | undefined | Promise; /** * USD caps for the resolved user. Called once per request (per agent * step, more precisely). Caps may be set per-tier / per-plan by reading * the user from your DB inside the callback. */ budget(args: { userId: string; ctx: MiddlewareContext; }): BudgetCaps | Promise; /** * Where counters persist. Use {@link memoryBudgetStorage} for tests + * single-process dev; `ormBudgetStorage` (#A6 Phase 4) for production. */ storage: BudgetStorage; /** * Pricing catalog. Defaults to the shipped {@link ModelPricing}; spread * to override entries for negotiated rates: * * ```ts * pricing: { ...ModelPricing, 'anthropic/claude-opus-4-7': { ... } } * ``` */ pricing?: Record; /** * Called when a debit would exceed a cap. Default throws * {@link BudgetExceededError}; supply your own to log/alert before * throwing, or to throw a different error class. Must throw — return * value is ignored. The throw aborts the agent run before the model * call. */ onExceeded?(args: BudgetExceededArgs): never | Promise; /** * IANA timezone for daily / monthly period rollover. Defaults to UTC. * Use the user's tz to roll caps at user-local midnight (matches * billing dashboards). */ timezone?: string; /** * Approximate-tokens estimator used for the pre-debit. Defaults to * `Math.ceil(text.length / 4)` — fine for English-heavy prompts. Pass * a tiktoken-backed estimator for accuracy. */ estimateTokens?: (text: string) => number; } /** * Per-user spend cap middleware (#A6 Phase 3). * * Pre-debits an input-cost estimate before each provider call (refusing * with {@link BudgetExceededError} if the user would exceed any * configured cap), then debits the actual cost difference once the * `usage` chunk arrives. * * The pre-debit reserves budget so two concurrent requests can't both * pass the check before either is billed — the `BudgetStorage` contract * (#A6 Phase 2) requires `checkAndDebit` to be atomic. * * # Example * * ```ts * import { withBudget, memoryBudgetStorage, ModelPricing } from '@gemstack/ai-sdk' * * const budgeted = withBudget({ * user: (ctx) => ctx.context as string, // your app's user-id source * budget: () => ({ daily: 0.50, monthly: 10 }), // USD * storage: memoryBudgetStorage(), * pricing: ModelPricing, * }) * * class MyAgent extends Agent { * middleware() { return [budgeted] } * } * ``` * * # Caveats * * - **Refunds on errors are not issued.** If the provider call fails * after the pre-debit, the estimate stays debited. This avoids the * complexity of distinguishing partial-credit cases (the model may * have produced output before erroring). Apps that need refund-on-error * should subscribe via `onError` and call `storage` directly. * - **Cache token deltas not counted.** `TokenUsage` does not yet expose * `cacheReadInputTokens` / `cacheWriteInputTokens`; cached requests are * billed at the full `inputPer1k` rate today. Refining this is a phase * 3.x follow-up that needs a `TokenUsage` widening. * - **Tokenizer differences.** The default token estimator is * `text.length / 4`. Provider-reported `usage.promptTokens` may differ * by a few percent. Pass `estimateTokens: …` for a tiktoken-accurate * pre-debit if your caps are tight. */ export declare function withBudget(opts: WithBudgetOptions): AiMiddleware; //# sourceMappingURL=with-budget.d.ts.map