@convex-dev/ai-budget 0.0.2-alpha.4 → 0.0.2-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,11 @@ full audit log you can replay later.
26
26
  | **Usage & cost tracking** | Every request stored with messages, response, tokens, latency, and per-request cost. |
27
27
  | **Attribution** | Each call is attributed to a `userId` **and** to the Convex action that made it — auto-detected via `ctx.meta`, no manual tagging. Running totals per user and per action. |
28
28
  | **Tagged budgets** | `user` and `action` are just built-in *dimensions* — add your own (team, project, customer, env, feature…) by passing `tags`, and cap any of them with `ai.tag("customer").setLimits(...)`. One request can be billed to several buckets at once. |
29
- | **Spend & token limits** | Per-user daily / lifetime **spend** and **token** budgets, plus a requests-per-minute rate limit and a block switch. |
29
+ | **Spend & token limits** | Per-bucket **daily / monthly / lifetime** spend and token budgets, plus a requests-per-minute rate limit, a max-concurrent cap, and a block switch. |
30
+ | **Spend history** | Durable per-bucket **daily & monthly** rollups that survive request retention — real spend-over-time, "what did we spend last month," per user / action / tag. |
31
+ | **Approaching-limit alerts** | Set `warnAtPct` (e.g. 0.8) and get an `onThreshold` callback before a cap is hit; `onLimitReached` fires when one blocks. |
32
+ | **Manual credits/debits** | Comp a user or correct an overcharge with a signed adjustment; the change hits the live windows, the history, and an audit log. |
33
+ | **Cache-aware cost** | Cached (prompt-cache-read) tokens are billed at a discount using the gateway's real cached-token count — not the full input rate. |
30
34
  | **Concurrency-safe caps** | A reserve-then-settle design makes admission a true atomic check — concurrent in-flight requests can't blow past the cap (a naive implementation overshoots ~40×). |
31
35
  | **Hard or soft** | Each limit either **blocks** (`hard`) or **allows-with-a-warning** (`soft`). |
32
36
  | **Per-feature budgets** | Cap or block a whole action (e.g. `summarize`) independently of any user. |
@@ -186,16 +190,21 @@ to the original. `lineage` walks the re-run chain in both directions.
186
190
  ai.users.setLimits(ctx, {
187
191
  userId,
188
192
  requestsPerMinute?,
193
+ maxConcurrent?, // max in-flight requests at once
189
194
  dailySpendLimitNanos?,
195
+ monthlySpendLimitNanos?, // calendar-month budget (UTC)
190
196
  lifetimeSpendLimitNanos?,
191
197
  dailyTokenLimit?,
198
+ monthlyTokenLimit?,
192
199
  lifetimeTokenLimit?,
193
- enforcement?, // "hard" (block, default) | "soft" (warn but allow)
194
- blocked?, // hard block on/off
200
+ warnAtPct?, // e.g. 0.8 fire onThreshold at 80% of a cap
201
+ enforcement?, // "hard" (block, default) | "soft" (warn but allow)
202
+ blocked?, // hard block on/off
195
203
  })
196
204
  ai.users.delete(ctx, { userId }) // remove a user and all their request rows
197
205
  ```
198
206
 
207
+ The same limit fields apply to `ai.actions.setLimits` and `ai.tag(d).setLimits`.
199
208
  Pass a field as `undefined` to clear that limit (unlimited).
200
209
 
201
210
  ### Per-action budgets
@@ -246,6 +255,48 @@ Uncapped buckets never serialize, so adding tags you don't cap is free at
246
255
  admission; their running totals still accrue for reporting. `ai.users.*` and
247
256
  `ai.actions.*` are simply sugar over `ai.tag("user")` / `ai.tag("action")`.
248
257
 
258
+ Every dimension namespace (`users`, `actions`, `tag(d)`) shares the same methods:
259
+ `list`, `get`, `setLimits`, `bump`, `adjust`, `history`, `adjustments`, `delete`.
260
+
261
+ ### Spend history (survives retention)
262
+
263
+ Request rows are retained only briefly (see retention), but **durable per-bucket
264
+ day/month rollups are not** — so charts and "what did we spend last month" keep
265
+ working:
266
+
267
+ ```ts
268
+ await ai.users.history(ctx, { userId, period: "month" }); // [{ stamp, spendNanos, tokens, requests }]
269
+ await ai.tag("customer").history(ctx, { value: "acme", period: "day", limit: 30 });
270
+ ```
271
+
272
+ ### Approaching-limit alerts
273
+
274
+ Set a threshold (per bucket via `warnAtPct`, or a deployment default) and get a
275
+ callback before a cap is hit — plus one when a hard cap blocks:
276
+
277
+ ```ts
278
+ await ai.global.setAlertDefaults(ctx, { warnAtPct: 0.8 }); // 80%, all buckets
279
+ await ai.users.setLimits(ctx, { userId, warnAtPct: 0.9 }); // override per bucket
280
+
281
+ new AIBudget(components.aiBudget, {
282
+ onThreshold: ({ userId, messages }) => notify(userId, messages), // approaching
283
+ onLimitReached: ({ userId, reason }) => notify(userId, reason), // blocked
284
+ onSoftLimit: ({ userId, messages }) => notify(userId, messages), // soft-cap exceeded
285
+ });
286
+ ```
287
+
288
+ `chat()` also returns `notices` (approaching) alongside `warnings` (soft-exceeded).
289
+
290
+ ### Manual credits & debits
291
+
292
+ Comp a user or correct an overcharge. Negative = credit, positive = extra charge;
293
+ it adjusts the live day/month/lifetime windows, the history, and an audit log:
294
+
295
+ ```ts
296
+ await ai.users.adjust(ctx, { userId, deltaNanos: -5 * 1_000_000_000, reason: "goodwill" });
297
+ await ai.users.adjustments(ctx, { userId }); // the audit log
298
+ ```
299
+
249
300
  ### Global (deployment-wide) budget
250
301
 
251
302
  ```ts
@@ -283,10 +334,18 @@ common models (validated against OpenRouter's public pricing); override or add
283
334
  any model:
284
335
 
285
336
  ```ts
286
- ai.prices.set(ctx, { model, inputNanosPerMTok, outputNanosPerMTok }) // must be ≥ 0
337
+ ai.prices.set(ctx, { model, inputNanosPerMTok, outputNanosPerMTok, cachedNanosPerMTok? }) // ≥ 0
287
338
  ai.prices.list(ctx)
288
339
  ```
289
340
 
341
+ **Cached tokens & actual cost.** The gateway reports a real cached-token count
342
+ (`usage.inputTokenDetails.cacheReadTokens`), so the cached slice of a prompt is
343
+ billed at `cachedNanosPerMTok` (or a default 10%-of-input discount) instead of
344
+ the full input rate. The gateway does **not** currently return a dollar cost, so
345
+ cost is computed from tokens — but `finishRequest` accepts an authoritative
346
+ `costNanos` and prefers it whenever present, so adopting a real gateway cost is a
347
+ one-line change the day it's available.
348
+
290
349
  **Real prices from an API.** The gateway's `provider/model` ids match
291
350
  OpenRouter's, whose public models endpoint returns per-token pricing — so you can
292
351
  keep prices current from your own action (this is app code; the component just
@@ -311,10 +370,12 @@ flagged `unpricedModel: true` so you know to add a real price.
311
370
  ### Observability
312
371
 
313
372
  ```ts
314
- ai.users.list(ctx) // per-user spend today / total / tokens / limits
373
+ ai.users.list(ctx) // per-user spend today / month / total / limits
315
374
  ai.actions.list(ctx) // per-action spend & totals
316
375
  ai.tag("customer").list(ctx) // spend & caps for any custom dimension
317
- ai.requests.list(ctx, { userId?, limit? }) // the audit log (blocked attempts included)
376
+ ai.users.history(ctx, { userId, period: "month" }) // durable spend-over-time
377
+ ai.requests.list(ctx, { userId?, limit? }) // the audit log (blocked included)
378
+ ai.requests.list(ctx, { dimension: "customer", value: "acme" }) // filter the log by any tag
318
379
  ```
319
380
 
320
381
  ---
@@ -21,22 +21,34 @@ export type Tag = {
21
21
  dimension: string;
22
22
  value: string;
23
23
  };
24
- /** Fired when a request is admitted over a *soft* limit. */
25
- export type SoftLimitInfo = {
24
+ /** Common shape for budget-event callbacks. */
25
+ export type BudgetEventInfo = {
26
26
  userId: string;
27
27
  action?: string;
28
28
  tags?: Tag[];
29
- requestId: string;
29
+ requestId?: string;
30
+ /** soft-cap warnings (onSoftLimit) or approaching-cap notices (onThreshold). */
31
+ messages: string[];
32
+ /** rejection code/reason (onLimitReached only). */
33
+ code?: string;
34
+ reason?: string;
35
+ };
36
+ /** @deprecated use BudgetEventInfo */
37
+ export type SoftLimitInfo = BudgetEventInfo & {
30
38
  warnings: string[];
31
39
  };
32
40
  export type AIBudgetOptions = {
33
41
  defaultModel?: string;
34
42
  /**
35
- * Called when a soft limit is exceeded (the request is still allowed). Lets
36
- * you surface budget warnings even on the languageModel/Agent path, where
37
- * they can't be returned. Errors thrown here are swallowed.
43
+ * A *soft* limit was exceeded (request still allowed). Lets you surface budget
44
+ * warnings even on the languageModel/Agent path where they can't be returned.
45
+ * Errors thrown in any of these callbacks are swallowed.
38
46
  */
39
47
  onSoftLimit?: (info: SoftLimitInfo) => void | Promise<void>;
48
+ /** Usage crossed a bucket's warnAtPct threshold (approaching a cap). */
49
+ onThreshold?: (info: BudgetEventInfo) => void | Promise<void>;
50
+ /** A *hard* limit blocked the request (fires just before chat/model throws). */
51
+ onLimitReached?: (info: BudgetEventInfo) => void | Promise<void>;
40
52
  };
41
53
  type RunQueryCtx = {
42
54
  runQuery: <Query extends FunctionReference<"query", "internal">>(query: Query, args: Query["_args"]) => Promise<Query["_returnType"]>;
@@ -67,13 +79,39 @@ export type ChatResult = {
67
79
  cachedTokens: number;
68
80
  /** Soft-limit warnings raised at admission (empty unless a soft cap was hit). */
69
81
  warnings: string[];
82
+ /** Approaching-cap notices (empty unless a warnAtPct threshold was crossed). */
83
+ notices: string[];
84
+ };
85
+ /** Limits/controls settable on any budget bucket (user, action, or tag). */
86
+ export type BucketLimits = {
87
+ requestsPerMinute?: number;
88
+ maxConcurrent?: number;
89
+ dailySpendLimitNanos?: number;
90
+ monthlySpendLimitNanos?: number;
91
+ lifetimeSpendLimitNanos?: number;
92
+ dailyTokenLimit?: number;
93
+ monthlyTokenLimit?: number;
94
+ lifetimeTokenLimit?: number;
95
+ /** Fire an approaching-limit alert at this fraction of a cap (e.g. 0.8). */
96
+ warnAtPct?: number;
97
+ enforcement?: "hard" | "soft";
98
+ blocked?: boolean;
99
+ };
100
+ /** One-time bump amounts, added on top of a standing cap. */
101
+ export type BumpArgs = {
102
+ dailyNanos?: number;
103
+ monthlyNanos?: number;
104
+ lifetimeNanos?: number;
70
105
  };
71
106
  export declare class AIBudget {
72
107
  component: AIBudgetApi;
73
108
  defaultModel: string;
74
109
  private onSoftLimit?;
110
+ private onThreshold?;
111
+ private onLimitReached?;
75
112
  constructor(component: AIBudgetApi, options?: AIBudgetOptions);
76
- private fireSoftLimit;
113
+ private fireBudgetEvents;
114
+ private fireLimitReached;
77
115
  /**
78
116
  * One-shot chat through the AI Gateway with tracking + limits.
79
117
  * Call from an action. `userId` defaults to the authenticated caller.
@@ -106,8 +144,11 @@ export declare class AIBudget {
106
144
  private rerunImpl;
107
145
  /** The request audit log, replay, and re-run lineage. */
108
146
  get requests(): {
147
+ /** Filter by userId, or by any {dimension, value} (incl. custom tags). */
109
148
  list: (ctx: RunQueryCtx, args?: {
110
149
  userId?: string;
150
+ dimension?: string;
151
+ value?: string;
111
152
  limit?: number;
112
153
  }) => Promise<{
113
154
  _id: string;
@@ -213,8 +254,8 @@ export declare class AIBudget {
213
254
  * generalization of `users`/`actions`. Give it any dimension name (team,
214
255
  * project, tenant, customer, env, feature, …) and set caps per value:
215
256
  *
216
- * ai.tag("customer").setLimits(ctx, { value: "acme", dailySpendLimitNanos });
217
- * ai.tag("customer").list(ctx);
257
+ * ai.tag("customer").setLimits(ctx, { value: "acme", monthlySpendLimitNanos });
258
+ * ai.tag("customer").history(ctx, { value: "acme", period: "day" });
218
259
  *
219
260
  * Attribute a call to it by passing `tags` to `chat`/`languageModel`.
220
261
  */
@@ -222,22 +263,33 @@ export declare class AIBudget {
222
263
  /** All buckets in this dimension. */
223
264
  list: (ctx: RunQueryCtx) => Promise<{
224
265
  spendTodayNanos: number;
266
+ spendThisMonthNanos: number;
225
267
  _id: string;
226
268
  _creationTime: number;
227
269
  requestsPerMinute?: number | undefined;
270
+ maxConcurrent?: number | undefined;
228
271
  dailySpendLimitNanos?: number | undefined;
272
+ monthlySpendLimitNanos?: number | undefined;
229
273
  lifetimeSpendLimitNanos?: number | undefined;
230
274
  dailyTokenLimit?: number | undefined;
275
+ monthlyTokenLimit?: number | undefined;
231
276
  lifetimeTokenLimit?: number | undefined;
232
277
  blocked?: boolean | undefined;
278
+ warnAtPct?: number | undefined;
233
279
  enforcement?: "hard" | "soft" | undefined;
234
280
  dailyBumpNanos?: number | undefined;
281
+ monthlyBumpNanos?: number | undefined;
235
282
  lifetimeBumpNanos?: number | undefined;
236
283
  bumpDayStamp?: string | undefined;
284
+ bumpMonthStamp?: string | undefined;
237
285
  tokensToday?: number | undefined;
286
+ monthStamp?: string | undefined;
287
+ tokensThisMonth?: number | undefined;
238
288
  reservedTodayNanos?: number | undefined;
289
+ reservedMonthNanos?: number | undefined;
239
290
  reservedTotalNanos?: number | undefined;
240
291
  reservedTodayTokens?: number | undefined;
292
+ reservedMonthTokens?: number | undefined;
241
293
  reservedTotalTokens?: number | undefined;
242
294
  pendingCount?: number | undefined;
243
295
  dimension: string;
@@ -252,22 +304,33 @@ export declare class AIBudget {
252
304
  value: string;
253
305
  }) => Promise<{
254
306
  spendTodayNanos: number;
307
+ spendThisMonthNanos: number;
255
308
  _id: string;
256
309
  _creationTime: number;
257
310
  requestsPerMinute?: number | undefined;
311
+ maxConcurrent?: number | undefined;
258
312
  dailySpendLimitNanos?: number | undefined;
313
+ monthlySpendLimitNanos?: number | undefined;
259
314
  lifetimeSpendLimitNanos?: number | undefined;
260
315
  dailyTokenLimit?: number | undefined;
316
+ monthlyTokenLimit?: number | undefined;
261
317
  lifetimeTokenLimit?: number | undefined;
262
318
  blocked?: boolean | undefined;
319
+ warnAtPct?: number | undefined;
263
320
  enforcement?: "hard" | "soft" | undefined;
264
321
  dailyBumpNanos?: number | undefined;
322
+ monthlyBumpNanos?: number | undefined;
265
323
  lifetimeBumpNanos?: number | undefined;
266
324
  bumpDayStamp?: string | undefined;
325
+ bumpMonthStamp?: string | undefined;
267
326
  tokensToday?: number | undefined;
327
+ monthStamp?: string | undefined;
328
+ tokensThisMonth?: number | undefined;
268
329
  reservedTodayNanos?: number | undefined;
330
+ reservedMonthNanos?: number | undefined;
269
331
  reservedTotalNanos?: number | undefined;
270
332
  reservedTodayTokens?: number | undefined;
333
+ reservedMonthTokens?: number | undefined;
271
334
  reservedTotalTokens?: number | undefined;
272
335
  pendingCount?: number | undefined;
273
336
  dimension: string;
@@ -279,20 +342,50 @@ export declare class AIBudget {
279
342
  } | null>;
280
343
  setLimits: (ctx: RunMutationCtx, args: {
281
344
  value: string;
282
- requestsPerMinute?: number;
283
- dailySpendLimitNanos?: number;
284
- lifetimeSpendLimitNanos?: number;
285
- dailyTokenLimit?: number;
286
- lifetimeTokenLimit?: number;
287
- enforcement?: "hard" | "soft";
288
- blocked?: boolean;
289
- }) => Promise<null>;
290
- /** One-time "approve another $X" bump (daily is today-only). */
345
+ } & BucketLimits) => Promise<null>;
346
+ /** One-time "approve another $X" bump (daily/monthly reset with the window). */
291
347
  bump: (ctx: RunMutationCtx, args: {
292
348
  value: string;
293
- dailyNanos?: number;
294
- lifetimeNanos?: number;
349
+ } & BumpArgs) => Promise<null>;
350
+ /** Manually credit (negative) or debit (positive) this bucket. */
351
+ adjust: (ctx: RunMutationCtx, args: {
352
+ value: string;
353
+ } & {
354
+ deltaNanos: number;
355
+ tokens?: number;
356
+ reason?: string;
295
357
  }) => Promise<null>;
358
+ /** Durable spend history for this bucket (per day or per month). */
359
+ history: (ctx: RunQueryCtx, args: {
360
+ value: string;
361
+ } & {
362
+ period?: "day" | "month";
363
+ limit?: number;
364
+ }) => Promise<{
365
+ _id: string;
366
+ _creationTime: number;
367
+ dimension: string;
368
+ value: string;
369
+ period: "day" | "month";
370
+ stamp: string;
371
+ spendNanos: number;
372
+ tokens: number;
373
+ requests: number;
374
+ }[]>;
375
+ /** Manual-adjustment audit log for this bucket. */
376
+ adjustments: (ctx: RunQueryCtx, args: {
377
+ value: string;
378
+ } & {
379
+ limit?: number;
380
+ }) => Promise<{
381
+ _id: string;
382
+ _creationTime: number;
383
+ tokens?: number | undefined;
384
+ reason?: string | undefined;
385
+ dimension: string;
386
+ value: string;
387
+ deltaNanos: number;
388
+ }[]>;
296
389
  /** Delete the bucket (for "user", also its request rows). */
297
390
  delete: (ctx: RunMutationCtx, args: {
298
391
  value: string;
@@ -301,26 +394,39 @@ export declare class AIBudget {
301
394
  done: boolean;
302
395
  }>;
303
396
  };
397
+ private dimensionApi;
304
398
  /** Per-user budgets and controls — sugar over the "user" dimension. */
305
399
  get users(): {
400
+ /** All buckets in this dimension. */
306
401
  list: (ctx: RunQueryCtx) => Promise<{
307
402
  spendTodayNanos: number;
403
+ spendThisMonthNanos: number;
308
404
  _id: string;
309
405
  _creationTime: number;
310
406
  requestsPerMinute?: number | undefined;
407
+ maxConcurrent?: number | undefined;
311
408
  dailySpendLimitNanos?: number | undefined;
409
+ monthlySpendLimitNanos?: number | undefined;
312
410
  lifetimeSpendLimitNanos?: number | undefined;
313
411
  dailyTokenLimit?: number | undefined;
412
+ monthlyTokenLimit?: number | undefined;
314
413
  lifetimeTokenLimit?: number | undefined;
315
414
  blocked?: boolean | undefined;
415
+ warnAtPct?: number | undefined;
316
416
  enforcement?: "hard" | "soft" | undefined;
317
417
  dailyBumpNanos?: number | undefined;
418
+ monthlyBumpNanos?: number | undefined;
318
419
  lifetimeBumpNanos?: number | undefined;
319
420
  bumpDayStamp?: string | undefined;
421
+ bumpMonthStamp?: string | undefined;
320
422
  tokensToday?: number | undefined;
423
+ monthStamp?: string | undefined;
424
+ tokensThisMonth?: number | undefined;
321
425
  reservedTodayNanos?: number | undefined;
426
+ reservedMonthNanos?: number | undefined;
322
427
  reservedTotalNanos?: number | undefined;
323
428
  reservedTodayTokens?: number | undefined;
429
+ reservedMonthTokens?: number | undefined;
324
430
  reservedTotalTokens?: number | undefined;
325
431
  pendingCount?: number | undefined;
326
432
  dimension: string;
@@ -330,23 +436,94 @@ export declare class AIBudget {
330
436
  totalTokens: number;
331
437
  dayStamp: string;
332
438
  }[]>;
439
+ /** One bucket's limits + spend (null if it has none yet). */
440
+ get: (ctx: RunQueryCtx, args: {
441
+ userId: string;
442
+ }) => Promise<{
443
+ spendTodayNanos: number;
444
+ spendThisMonthNanos: number;
445
+ _id: string;
446
+ _creationTime: number;
447
+ requestsPerMinute?: number | undefined;
448
+ maxConcurrent?: number | undefined;
449
+ dailySpendLimitNanos?: number | undefined;
450
+ monthlySpendLimitNanos?: number | undefined;
451
+ lifetimeSpendLimitNanos?: number | undefined;
452
+ dailyTokenLimit?: number | undefined;
453
+ monthlyTokenLimit?: number | undefined;
454
+ lifetimeTokenLimit?: number | undefined;
455
+ blocked?: boolean | undefined;
456
+ warnAtPct?: number | undefined;
457
+ enforcement?: "hard" | "soft" | undefined;
458
+ dailyBumpNanos?: number | undefined;
459
+ monthlyBumpNanos?: number | undefined;
460
+ lifetimeBumpNanos?: number | undefined;
461
+ bumpDayStamp?: string | undefined;
462
+ bumpMonthStamp?: string | undefined;
463
+ tokensToday?: number | undefined;
464
+ monthStamp?: string | undefined;
465
+ tokensThisMonth?: number | undefined;
466
+ reservedTodayNanos?: number | undefined;
467
+ reservedMonthNanos?: number | undefined;
468
+ reservedTotalNanos?: number | undefined;
469
+ reservedTodayTokens?: number | undefined;
470
+ reservedMonthTokens?: number | undefined;
471
+ reservedTotalTokens?: number | undefined;
472
+ pendingCount?: number | undefined;
473
+ dimension: string;
474
+ value: string;
475
+ totalSpendNanos: number;
476
+ totalRequests: number;
477
+ totalTokens: number;
478
+ dayStamp: string;
479
+ } | null>;
333
480
  setLimits: (ctx: RunMutationCtx, args: {
334
481
  userId: string;
335
- requestsPerMinute?: number;
336
- dailySpendLimitNanos?: number;
337
- lifetimeSpendLimitNanos?: number;
338
- dailyTokenLimit?: number;
339
- lifetimeTokenLimit?: number;
340
- enforcement?: "hard" | "soft";
341
- blocked?: boolean;
342
- }) => Promise<null>;
343
- /** One-time "approve another $X" bump (daily is today-only). */
482
+ } & BucketLimits) => Promise<null>;
483
+ /** One-time "approve another $X" bump (daily/monthly reset with the window). */
344
484
  bump: (ctx: RunMutationCtx, args: {
345
485
  userId: string;
346
- dailyNanos?: number;
347
- lifetimeNanos?: number;
486
+ } & BumpArgs) => Promise<null>;
487
+ /** Manually credit (negative) or debit (positive) this bucket. */
488
+ adjust: (ctx: RunMutationCtx, args: {
489
+ userId: string;
490
+ } & {
491
+ deltaNanos: number;
492
+ tokens?: number;
493
+ reason?: string;
348
494
  }) => Promise<null>;
349
- /** Delete a user and all their request rows. */
495
+ /** Durable spend history for this bucket (per day or per month). */
496
+ history: (ctx: RunQueryCtx, args: {
497
+ userId: string;
498
+ } & {
499
+ period?: "day" | "month";
500
+ limit?: number;
501
+ }) => Promise<{
502
+ _id: string;
503
+ _creationTime: number;
504
+ dimension: string;
505
+ value: string;
506
+ period: "day" | "month";
507
+ stamp: string;
508
+ spendNanos: number;
509
+ tokens: number;
510
+ requests: number;
511
+ }[]>;
512
+ /** Manual-adjustment audit log for this bucket. */
513
+ adjustments: (ctx: RunQueryCtx, args: {
514
+ userId: string;
515
+ } & {
516
+ limit?: number;
517
+ }) => Promise<{
518
+ _id: string;
519
+ _creationTime: number;
520
+ tokens?: number | undefined;
521
+ reason?: string | undefined;
522
+ dimension: string;
523
+ value: string;
524
+ deltaNanos: number;
525
+ }[]>;
526
+ /** Delete the bucket (for "user", also its request rows). */
350
527
  delete: (ctx: RunMutationCtx, args: {
351
528
  userId: string;
352
529
  }) => Promise<{
@@ -356,24 +533,36 @@ export declare class AIBudget {
356
533
  };
357
534
  /** Per-action (per-feature) budgets — sugar over the "action" dimension. */
358
535
  get actions(): {
536
+ /** All buckets in this dimension. */
359
537
  list: (ctx: RunQueryCtx) => Promise<{
360
538
  spendTodayNanos: number;
539
+ spendThisMonthNanos: number;
361
540
  _id: string;
362
541
  _creationTime: number;
363
542
  requestsPerMinute?: number | undefined;
543
+ maxConcurrent?: number | undefined;
364
544
  dailySpendLimitNanos?: number | undefined;
545
+ monthlySpendLimitNanos?: number | undefined;
365
546
  lifetimeSpendLimitNanos?: number | undefined;
366
547
  dailyTokenLimit?: number | undefined;
548
+ monthlyTokenLimit?: number | undefined;
367
549
  lifetimeTokenLimit?: number | undefined;
368
550
  blocked?: boolean | undefined;
551
+ warnAtPct?: number | undefined;
369
552
  enforcement?: "hard" | "soft" | undefined;
370
553
  dailyBumpNanos?: number | undefined;
554
+ monthlyBumpNanos?: number | undefined;
371
555
  lifetimeBumpNanos?: number | undefined;
372
556
  bumpDayStamp?: string | undefined;
557
+ bumpMonthStamp?: string | undefined;
373
558
  tokensToday?: number | undefined;
559
+ monthStamp?: string | undefined;
560
+ tokensThisMonth?: number | undefined;
374
561
  reservedTodayNanos?: number | undefined;
562
+ reservedMonthNanos?: number | undefined;
375
563
  reservedTotalNanos?: number | undefined;
376
564
  reservedTodayTokens?: number | undefined;
565
+ reservedMonthTokens?: number | undefined;
377
566
  reservedTotalTokens?: number | undefined;
378
567
  pendingCount?: number | undefined;
379
568
  dimension: string;
@@ -383,23 +572,102 @@ export declare class AIBudget {
383
572
  totalTokens: number;
384
573
  dayStamp: string;
385
574
  }[]>;
575
+ /** One bucket's limits + spend (null if it has none yet). */
576
+ get: (ctx: RunQueryCtx, args: {
577
+ name: string;
578
+ }) => Promise<{
579
+ spendTodayNanos: number;
580
+ spendThisMonthNanos: number;
581
+ _id: string;
582
+ _creationTime: number;
583
+ requestsPerMinute?: number | undefined;
584
+ maxConcurrent?: number | undefined;
585
+ dailySpendLimitNanos?: number | undefined;
586
+ monthlySpendLimitNanos?: number | undefined;
587
+ lifetimeSpendLimitNanos?: number | undefined;
588
+ dailyTokenLimit?: number | undefined;
589
+ monthlyTokenLimit?: number | undefined;
590
+ lifetimeTokenLimit?: number | undefined;
591
+ blocked?: boolean | undefined;
592
+ warnAtPct?: number | undefined;
593
+ enforcement?: "hard" | "soft" | undefined;
594
+ dailyBumpNanos?: number | undefined;
595
+ monthlyBumpNanos?: number | undefined;
596
+ lifetimeBumpNanos?: number | undefined;
597
+ bumpDayStamp?: string | undefined;
598
+ bumpMonthStamp?: string | undefined;
599
+ tokensToday?: number | undefined;
600
+ monthStamp?: string | undefined;
601
+ tokensThisMonth?: number | undefined;
602
+ reservedTodayNanos?: number | undefined;
603
+ reservedMonthNanos?: number | undefined;
604
+ reservedTotalNanos?: number | undefined;
605
+ reservedTodayTokens?: number | undefined;
606
+ reservedMonthTokens?: number | undefined;
607
+ reservedTotalTokens?: number | undefined;
608
+ pendingCount?: number | undefined;
609
+ dimension: string;
610
+ value: string;
611
+ totalSpendNanos: number;
612
+ totalRequests: number;
613
+ totalTokens: number;
614
+ dayStamp: string;
615
+ } | null>;
386
616
  setLimits: (ctx: RunMutationCtx, args: {
387
617
  name: string;
388
- dailySpendLimitNanos?: number;
389
- lifetimeSpendLimitNanos?: number;
390
- dailyTokenLimit?: number;
391
- lifetimeTokenLimit?: number;
392
- enforcement?: "hard" | "soft";
393
- /** Hard-block this action (was `disabled`). */
394
- blocked?: boolean;
395
- }) => Promise<null>;
618
+ } & BucketLimits) => Promise<null>;
619
+ /** One-time "approve another $X" bump (daily/monthly reset with the window). */
396
620
  bump: (ctx: RunMutationCtx, args: {
397
621
  name: string;
398
- dailyNanos?: number;
399
- lifetimeNanos?: number;
622
+ } & BumpArgs) => Promise<null>;
623
+ /** Manually credit (negative) or debit (positive) this bucket. */
624
+ adjust: (ctx: RunMutationCtx, args: {
625
+ name: string;
626
+ } & {
627
+ deltaNanos: number;
628
+ tokens?: number;
629
+ reason?: string;
400
630
  }) => Promise<null>;
631
+ /** Durable spend history for this bucket (per day or per month). */
632
+ history: (ctx: RunQueryCtx, args: {
633
+ name: string;
634
+ } & {
635
+ period?: "day" | "month";
636
+ limit?: number;
637
+ }) => Promise<{
638
+ _id: string;
639
+ _creationTime: number;
640
+ dimension: string;
641
+ value: string;
642
+ period: "day" | "month";
643
+ stamp: string;
644
+ spendNanos: number;
645
+ tokens: number;
646
+ requests: number;
647
+ }[]>;
648
+ /** Manual-adjustment audit log for this bucket. */
649
+ adjustments: (ctx: RunQueryCtx, args: {
650
+ name: string;
651
+ } & {
652
+ limit?: number;
653
+ }) => Promise<{
654
+ _id: string;
655
+ _creationTime: number;
656
+ tokens?: number | undefined;
657
+ reason?: string | undefined;
658
+ dimension: string;
659
+ value: string;
660
+ deltaNanos: number;
661
+ }[]>;
662
+ /** Delete the bucket (for "user", also its request rows). */
663
+ delete: (ctx: RunMutationCtx, args: {
664
+ name: string;
665
+ }) => Promise<{
666
+ deletedThisBatch: number;
667
+ done: boolean;
668
+ }>;
401
669
  };
402
- /** The deployment-wide budget and retention config. */
670
+ /** The deployment-wide budget, alerts, and retention config. */
403
671
  get global(): {
404
672
  /** Limits + spend today/total. */
405
673
  status: (ctx: RunQueryCtx) => Promise<{
@@ -419,6 +687,10 @@ export declare class AIBudget {
419
687
  dailyNanos?: number;
420
688
  lifetimeNanos?: number;
421
689
  }) => Promise<null>;
690
+ /** Default approaching-limit alert threshold (fraction of a cap, e.g. 0.8). */
691
+ setAlertDefaults: (ctx: RunMutationCtx, args: {
692
+ warnAtPct?: number;
693
+ }) => Promise<null>;
422
694
  /** Request-row retention window in ms (default 1h; 0 disables). */
423
695
  setRetention: (ctx: RunMutationCtx, args: {
424
696
  retentionMs: number;
@@ -442,6 +714,7 @@ export declare class AIBudget {
442
714
  [x: string]: {
443
715
  input: number;
444
716
  output: number;
717
+ cached?: number | undefined;
445
718
  overridden: boolean;
446
719
  };
447
720
  }>;
@@ -449,6 +722,8 @@ export declare class AIBudget {
449
722
  model: string;
450
723
  inputNanosPerMTok: number;
451
724
  outputNanosPerMTok: number;
725
+ /** Cache-read rate; defaults to a discount off input if omitted. */
726
+ cachedNanosPerMTok?: number;
452
727
  }) => Promise<null>;
453
728
  };
454
729
  }