@convex-dev/ai-budget 0.0.2-alpha.0 → 0.0.2-alpha.12

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.
@@ -1,4 +1,4 @@
1
- import type { Expand, FunctionReference } from "convex/server";
1
+ import { type Expand, type FunctionReference, type HttpRouter } from "convex/server";
2
2
  import { type GenericId } from "convex/values";
3
3
  import { type LanguageModel } from "ai";
4
4
  import type { api } from "../component/_generated/api";
@@ -11,21 +11,44 @@ type UseApi<API> = Expand<{
11
11
  export type AIBudgetApi = UseApi<typeof api>;
12
12
  /** @deprecated use AIBudgetApi */
13
13
  export type AIGatewayApi = AIBudgetApi;
14
- /** Fired when a request is admitted over a *soft* limit. */
15
- export type SoftLimitInfo = {
14
+ /**
15
+ * One attribution tag: a (dimension, value) pair, e.g. {dimension:"customer",
16
+ * value:"acme"}. `user` and `action` are built-in dimensions (set via
17
+ * userId/action); use tags for anything else — team, project, tenant, env, ….
18
+ * Any tagged bucket can carry its own budget (see `ai.tag(dimension)`).
19
+ */
20
+ export type Tag = {
21
+ dimension: string;
22
+ value: string;
23
+ };
24
+ /** Common shape for budget-event callbacks. */
25
+ export type BudgetEventInfo = {
16
26
  userId: string;
17
27
  action?: string;
18
- requestId: string;
28
+ tags?: Tag[];
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 & {
19
38
  warnings: string[];
20
39
  };
21
40
  export type AIBudgetOptions = {
22
41
  defaultModel?: string;
23
42
  /**
24
- * Called when a soft limit is exceeded (the request is still allowed). Lets
25
- * you surface budget warnings even on the languageModel/Agent path, where
26
- * 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.
27
46
  */
28
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>;
29
52
  };
30
53
  type RunQueryCtx = {
31
54
  runQuery: <Query extends FunctionReference<"query", "internal">>(query: Query, args: Query["_args"]) => Promise<Query["_returnType"]>;
@@ -56,16 +79,69 @@ export type ChatResult = {
56
79
  cachedTokens: number;
57
80
  /** Soft-limit warnings raised at admission (empty unless a soft cap was hit). */
58
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;
59
105
  };
60
106
  export declare class AIBudget {
61
107
  component: AIBudgetApi;
62
108
  defaultModel: string;
63
109
  private onSoftLimit?;
110
+ private onThreshold?;
111
+ private onLimitReached?;
64
112
  constructor(component: AIBudgetApi, options?: AIBudgetOptions);
65
- private fireSoftLimit;
113
+ private fireBudgetEvents;
114
+ private fireLimitReached;
115
+ /**
116
+ * Meter ANY LLM call — gateway, a provider SDK, a raw fetch — with the same
117
+ * budgets, audit log, and cost tracking. Reserves before your `run` (throwing
118
+ * a ConvexError over a hard cap), runs it, then records the actual usage/cost.
119
+ * This is the provider-agnostic core; `chat` is sugar over it for the gateway.
120
+ *
121
+ * `run` returns what happened. Pass a raw provider `usage` object (auto-
122
+ * normalized) OR explicit `promptTokens`/`completionTokens`/`cachedTokens`,
123
+ * plus optional `serverToolUses` (e.g. `{ web_search: 3 }`, priced on top of
124
+ * tokens) and an authoritative `costNanos` (used verbatim if present).
125
+ */
126
+ meter(ctx: RunMutationCtx, opts: {
127
+ model: string;
128
+ messages: Message[];
129
+ userId?: string;
130
+ action?: string;
131
+ tags?: Tag[];
132
+ rerunOf?: string;
133
+ }, run: () => Promise<{
134
+ text?: string;
135
+ usage?: any;
136
+ promptTokens?: number;
137
+ completionTokens?: number;
138
+ cachedTokens?: number;
139
+ serverToolUses?: Record<string, number>;
140
+ costNanos?: number;
141
+ }>): Promise<ChatResult>;
66
142
  /**
67
- * One-shot chat through the AI Gateway with tracking + limits.
68
- * Call from an action. `userId` defaults to the authenticated caller.
143
+ * One-shot chat through the AI Gateway with tracking + limits — sugar over
144
+ * `meter`. Call from an action. `userId` defaults to the authenticated caller.
69
145
  */
70
146
  chat(ctx: RunMutationCtx, args?: {
71
147
  /** Whom to bill. Defaults to the authenticated user (ctx.auth). */
@@ -76,6 +152,8 @@ export declare class AIBudget {
76
152
  rerunOf?: string;
77
153
  /** Attribute spend to this action name. Defaults to the calling Convex action. */
78
154
  action?: string;
155
+ /** Extra attribution dimensions to bill/limit (team, customer, env, …). */
156
+ tags?: Tag[];
79
157
  }): Promise<ChatResult>;
80
158
  /**
81
159
  * An AI SDK LanguageModel that enforces limits and records usage/cost for
@@ -87,17 +165,26 @@ export declare class AIBudget {
87
165
  userId?: string;
88
166
  model?: string;
89
167
  action?: string;
168
+ /** Extra attribution dimensions to bill/limit (team, customer, env, …). */
169
+ tags?: Tag[];
90
170
  }): LanguageModel;
91
171
  private rerunImpl;
92
172
  /** The request audit log, replay, and re-run lineage. */
93
173
  get requests(): {
174
+ /** Filter by userId, or by any {dimension, value} (incl. custom tags). */
94
175
  list: (ctx: RunQueryCtx, args?: {
95
176
  userId?: string;
177
+ dimension?: string;
178
+ value?: string;
96
179
  limit?: number;
97
180
  }) => Promise<{
98
181
  _id: string;
99
182
  _creationTime: number;
100
183
  actionName?: string | undefined;
184
+ tags?: {
185
+ dimension: string;
186
+ value: string;
187
+ }[] | undefined;
101
188
  estimatedNanos?: number | undefined;
102
189
  estimatedTokens?: number | undefined;
103
190
  unpricedModel?: boolean | undefined;
@@ -108,6 +195,9 @@ export declare class AIBudget {
108
195
  promptTokens?: number | undefined;
109
196
  completionTokens?: number | undefined;
110
197
  cachedTokens?: number | undefined;
198
+ serverToolUses?: {
199
+ [x: string]: number;
200
+ } | undefined;
111
201
  costNanos?: number | undefined;
112
202
  latencyMs?: number | undefined;
113
203
  rerunOf?: string | undefined;
@@ -119,6 +209,41 @@ export declare class AIBudget {
119
209
  }[];
120
210
  status: "blocked" | "pending" | "success" | "error";
121
211
  }[]>;
212
+ /** One request, including its stored prompt and response. */
213
+ get: (ctx: RunQueryCtx, args: {
214
+ requestId: string;
215
+ }) => Promise<{
216
+ _id: string;
217
+ _creationTime: number;
218
+ actionName?: string | undefined;
219
+ tags?: {
220
+ dimension: string;
221
+ value: string;
222
+ }[] | undefined;
223
+ estimatedNanos?: number | undefined;
224
+ estimatedTokens?: number | undefined;
225
+ unpricedModel?: boolean | undefined;
226
+ overBudget?: boolean | undefined;
227
+ settled?: boolean | undefined;
228
+ error?: string | undefined;
229
+ responseText?: string | undefined;
230
+ promptTokens?: number | undefined;
231
+ completionTokens?: number | undefined;
232
+ cachedTokens?: number | undefined;
233
+ serverToolUses?: {
234
+ [x: string]: number;
235
+ } | undefined;
236
+ costNanos?: number | undefined;
237
+ latencyMs?: number | undefined;
238
+ rerunOf?: string | undefined;
239
+ userId: string;
240
+ model: string;
241
+ messages: {
242
+ role: string;
243
+ content: string;
244
+ }[];
245
+ status: "blocked" | "pending" | "success" | "error";
246
+ } | null>;
122
247
  /** Ancestors up to the original, plus direct re-runs. */
123
248
  lineage: (ctx: RunQueryCtx, args: {
124
249
  requestId: string;
@@ -127,6 +252,10 @@ export declare class AIBudget {
127
252
  _id: string;
128
253
  _creationTime: number;
129
254
  actionName?: string | undefined;
255
+ tags?: {
256
+ dimension: string;
257
+ value: string;
258
+ }[] | undefined;
130
259
  estimatedNanos?: number | undefined;
131
260
  estimatedTokens?: number | undefined;
132
261
  unpricedModel?: boolean | undefined;
@@ -137,6 +266,9 @@ export declare class AIBudget {
137
266
  promptTokens?: number | undefined;
138
267
  completionTokens?: number | undefined;
139
268
  cachedTokens?: number | undefined;
269
+ serverToolUses?: {
270
+ [x: string]: number;
271
+ } | undefined;
140
272
  costNanos?: number | undefined;
141
273
  latencyMs?: number | undefined;
142
274
  rerunOf?: string | undefined;
@@ -152,6 +284,10 @@ export declare class AIBudget {
152
284
  _id: string;
153
285
  _creationTime: number;
154
286
  actionName?: string | undefined;
287
+ tags?: {
288
+ dimension: string;
289
+ value: string;
290
+ }[] | undefined;
155
291
  estimatedNanos?: number | undefined;
156
292
  estimatedTokens?: number | undefined;
157
293
  unpricedModel?: boolean | undefined;
@@ -162,6 +298,9 @@ export declare class AIBudget {
162
298
  promptTokens?: number | undefined;
163
299
  completionTokens?: number | undefined;
164
300
  cachedTokens?: number | undefined;
301
+ serverToolUses?: {
302
+ [x: string]: number;
303
+ } | undefined;
165
304
  costNanos?: number | undefined;
166
305
  latencyMs?: number | undefined;
167
306
  rerunOf?: string | undefined;
@@ -181,51 +320,281 @@ export declare class AIBudget {
181
320
  model?: string;
182
321
  }) => Promise<ChatResult>;
183
322
  };
184
- /** Per-user budgets and controls. */
323
+ /**
324
+ * Budgets and controls for an arbitrary attribution dimension — the
325
+ * generalization of `users`/`actions`. Give it any dimension name (team,
326
+ * project, tenant, customer, env, feature, …) and set caps per value:
327
+ *
328
+ * ai.tag("customer").setLimits(ctx, { value: "acme", monthlySpendLimitNanos });
329
+ * ai.tag("customer").history(ctx, { value: "acme", period: "day" });
330
+ *
331
+ * Attribute a call to it by passing `tags` to `chat`/`languageModel`.
332
+ */
333
+ tag(dimension: string): {
334
+ /** All buckets in this dimension. */
335
+ list: (ctx: RunQueryCtx) => Promise<{
336
+ spendTodayNanos: number;
337
+ spendThisMonthNanos: number;
338
+ _id: string;
339
+ _creationTime: number;
340
+ requestsPerMinute?: number | undefined;
341
+ maxConcurrent?: number | undefined;
342
+ dailySpendLimitNanos?: number | undefined;
343
+ monthlySpendLimitNanos?: number | undefined;
344
+ lifetimeSpendLimitNanos?: number | undefined;
345
+ dailyTokenLimit?: number | undefined;
346
+ monthlyTokenLimit?: number | undefined;
347
+ lifetimeTokenLimit?: number | undefined;
348
+ blocked?: boolean | undefined;
349
+ warnAtPct?: number | undefined;
350
+ enforcement?: "hard" | "soft" | undefined;
351
+ dailyBumpNanos?: number | undefined;
352
+ monthlyBumpNanos?: number | undefined;
353
+ lifetimeBumpNanos?: number | undefined;
354
+ bumpDayStamp?: string | undefined;
355
+ bumpMonthStamp?: string | undefined;
356
+ tokensToday?: number | undefined;
357
+ monthStamp?: string | undefined;
358
+ tokensThisMonth?: number | undefined;
359
+ reservedTodayNanos?: number | undefined;
360
+ reservedMonthNanos?: number | undefined;
361
+ reservedTotalNanos?: number | undefined;
362
+ reservedTodayTokens?: number | undefined;
363
+ reservedMonthTokens?: number | undefined;
364
+ reservedTotalTokens?: number | undefined;
365
+ pendingCount?: number | undefined;
366
+ dimension: string;
367
+ value: string;
368
+ totalSpendNanos: number;
369
+ totalRequests: number;
370
+ totalTokens: number;
371
+ dayStamp: string;
372
+ }[]>;
373
+ /** One bucket's limits + spend (null if it has none yet). */
374
+ get: (ctx: RunQueryCtx, args: {
375
+ value: string;
376
+ }) => Promise<{
377
+ spendTodayNanos: number;
378
+ spendThisMonthNanos: number;
379
+ _id: string;
380
+ _creationTime: number;
381
+ requestsPerMinute?: number | undefined;
382
+ maxConcurrent?: number | undefined;
383
+ dailySpendLimitNanos?: number | undefined;
384
+ monthlySpendLimitNanos?: number | undefined;
385
+ lifetimeSpendLimitNanos?: number | undefined;
386
+ dailyTokenLimit?: number | undefined;
387
+ monthlyTokenLimit?: number | undefined;
388
+ lifetimeTokenLimit?: number | undefined;
389
+ blocked?: boolean | undefined;
390
+ warnAtPct?: number | undefined;
391
+ enforcement?: "hard" | "soft" | undefined;
392
+ dailyBumpNanos?: number | undefined;
393
+ monthlyBumpNanos?: number | undefined;
394
+ lifetimeBumpNanos?: number | undefined;
395
+ bumpDayStamp?: string | undefined;
396
+ bumpMonthStamp?: string | undefined;
397
+ tokensToday?: number | undefined;
398
+ monthStamp?: string | undefined;
399
+ tokensThisMonth?: number | undefined;
400
+ reservedTodayNanos?: number | undefined;
401
+ reservedMonthNanos?: number | undefined;
402
+ reservedTotalNanos?: number | undefined;
403
+ reservedTodayTokens?: number | undefined;
404
+ reservedMonthTokens?: number | undefined;
405
+ reservedTotalTokens?: number | undefined;
406
+ pendingCount?: number | undefined;
407
+ dimension: string;
408
+ value: string;
409
+ totalSpendNanos: number;
410
+ totalRequests: number;
411
+ totalTokens: number;
412
+ dayStamp: string;
413
+ } | null>;
414
+ setLimits: (ctx: RunMutationCtx, args: {
415
+ value: string;
416
+ } & BucketLimits) => Promise<null>;
417
+ /** One-time "approve another $X" bump (daily/monthly reset with the window). */
418
+ bump: (ctx: RunMutationCtx, args: {
419
+ value: string;
420
+ } & BumpArgs) => Promise<null>;
421
+ /** Manually credit (negative) or debit (positive) this bucket. */
422
+ adjust: (ctx: RunMutationCtx, args: {
423
+ value: string;
424
+ } & {
425
+ deltaNanos: number;
426
+ tokens?: number;
427
+ reason?: string;
428
+ }) => Promise<null>;
429
+ /** Durable spend history for this bucket (per day or per month). */
430
+ history: (ctx: RunQueryCtx, args: {
431
+ value: string;
432
+ } & {
433
+ period?: "day" | "month";
434
+ limit?: number;
435
+ }) => Promise<{
436
+ _id: string;
437
+ _creationTime: number;
438
+ dimension: string;
439
+ value: string;
440
+ period: "day" | "month";
441
+ stamp: string;
442
+ spendNanos: number;
443
+ tokens: number;
444
+ requests: number;
445
+ }[]>;
446
+ /** Manual-adjustment audit log for this bucket. */
447
+ adjustments: (ctx: RunQueryCtx, args: {
448
+ value: string;
449
+ } & {
450
+ limit?: number;
451
+ }) => Promise<{
452
+ _id: string;
453
+ _creationTime: number;
454
+ tokens?: number | undefined;
455
+ reason?: string | undefined;
456
+ dimension: string;
457
+ value: string;
458
+ deltaNanos: number;
459
+ }[]>;
460
+ /** Delete the bucket (for "user", also its request rows). */
461
+ delete: (ctx: RunMutationCtx, args: {
462
+ value: string;
463
+ }) => Promise<{
464
+ deletedThisBatch: number;
465
+ done: boolean;
466
+ }>;
467
+ };
468
+ private dimensionApi;
469
+ /** Per-user budgets and controls — sugar over the "user" dimension. */
185
470
  get users(): {
471
+ /** All buckets in this dimension. */
186
472
  list: (ctx: RunQueryCtx) => Promise<{
187
473
  spendTodayNanos: number;
474
+ spendThisMonthNanos: number;
188
475
  _id: string;
189
476
  _creationTime: number;
190
477
  requestsPerMinute?: number | undefined;
478
+ maxConcurrent?: number | undefined;
191
479
  dailySpendLimitNanos?: number | undefined;
480
+ monthlySpendLimitNanos?: number | undefined;
192
481
  lifetimeSpendLimitNanos?: number | undefined;
193
482
  dailyTokenLimit?: number | undefined;
483
+ monthlyTokenLimit?: number | undefined;
194
484
  lifetimeTokenLimit?: number | undefined;
195
485
  blocked?: boolean | undefined;
486
+ warnAtPct?: number | undefined;
196
487
  enforcement?: "hard" | "soft" | undefined;
197
488
  dailyBumpNanos?: number | undefined;
489
+ monthlyBumpNanos?: number | undefined;
198
490
  lifetimeBumpNanos?: number | undefined;
199
491
  bumpDayStamp?: string | undefined;
492
+ bumpMonthStamp?: string | undefined;
200
493
  tokensToday?: number | undefined;
494
+ monthStamp?: string | undefined;
495
+ tokensThisMonth?: number | undefined;
201
496
  reservedTodayNanos?: number | undefined;
497
+ reservedMonthNanos?: number | undefined;
202
498
  reservedTotalNanos?: number | undefined;
203
499
  reservedTodayTokens?: number | undefined;
500
+ reservedMonthTokens?: number | undefined;
204
501
  reservedTotalTokens?: number | undefined;
205
502
  pendingCount?: number | undefined;
206
- userId: string;
503
+ dimension: string;
504
+ value: string;
207
505
  totalSpendNanos: number;
208
506
  totalRequests: number;
209
507
  totalTokens: number;
210
508
  dayStamp: string;
211
509
  }[]>;
510
+ /** One bucket's limits + spend (null if it has none yet). */
511
+ get: (ctx: RunQueryCtx, args: {
512
+ userId: string;
513
+ }) => Promise<{
514
+ spendTodayNanos: number;
515
+ spendThisMonthNanos: number;
516
+ _id: string;
517
+ _creationTime: number;
518
+ requestsPerMinute?: number | undefined;
519
+ maxConcurrent?: number | undefined;
520
+ dailySpendLimitNanos?: number | undefined;
521
+ monthlySpendLimitNanos?: number | undefined;
522
+ lifetimeSpendLimitNanos?: number | undefined;
523
+ dailyTokenLimit?: number | undefined;
524
+ monthlyTokenLimit?: number | undefined;
525
+ lifetimeTokenLimit?: number | undefined;
526
+ blocked?: boolean | undefined;
527
+ warnAtPct?: number | undefined;
528
+ enforcement?: "hard" | "soft" | undefined;
529
+ dailyBumpNanos?: number | undefined;
530
+ monthlyBumpNanos?: number | undefined;
531
+ lifetimeBumpNanos?: number | undefined;
532
+ bumpDayStamp?: string | undefined;
533
+ bumpMonthStamp?: string | undefined;
534
+ tokensToday?: number | undefined;
535
+ monthStamp?: string | undefined;
536
+ tokensThisMonth?: number | undefined;
537
+ reservedTodayNanos?: number | undefined;
538
+ reservedMonthNanos?: number | undefined;
539
+ reservedTotalNanos?: number | undefined;
540
+ reservedTodayTokens?: number | undefined;
541
+ reservedMonthTokens?: number | undefined;
542
+ reservedTotalTokens?: number | undefined;
543
+ pendingCount?: number | undefined;
544
+ dimension: string;
545
+ value: string;
546
+ totalSpendNanos: number;
547
+ totalRequests: number;
548
+ totalTokens: number;
549
+ dayStamp: string;
550
+ } | null>;
212
551
  setLimits: (ctx: RunMutationCtx, args: {
213
552
  userId: string;
214
- requestsPerMinute?: number;
215
- dailySpendLimitNanos?: number;
216
- lifetimeSpendLimitNanos?: number;
217
- dailyTokenLimit?: number;
218
- lifetimeTokenLimit?: number;
219
- enforcement?: "hard" | "soft";
220
- blocked?: boolean;
221
- }) => Promise<null>;
222
- /** One-time "approve another $X" bump (daily is today-only). */
553
+ } & BucketLimits) => Promise<null>;
554
+ /** One-time "approve another $X" bump (daily/monthly reset with the window). */
223
555
  bump: (ctx: RunMutationCtx, args: {
224
556
  userId: string;
225
- dailyNanos?: number;
226
- lifetimeNanos?: number;
557
+ } & BumpArgs) => Promise<null>;
558
+ /** Manually credit (negative) or debit (positive) this bucket. */
559
+ adjust: (ctx: RunMutationCtx, args: {
560
+ userId: string;
561
+ } & {
562
+ deltaNanos: number;
563
+ tokens?: number;
564
+ reason?: string;
227
565
  }) => Promise<null>;
228
- /** Delete a user and all their request rows. */
566
+ /** Durable spend history for this bucket (per day or per month). */
567
+ history: (ctx: RunQueryCtx, args: {
568
+ userId: string;
569
+ } & {
570
+ period?: "day" | "month";
571
+ limit?: number;
572
+ }) => Promise<{
573
+ _id: string;
574
+ _creationTime: number;
575
+ dimension: string;
576
+ value: string;
577
+ period: "day" | "month";
578
+ stamp: string;
579
+ spendNanos: number;
580
+ tokens: number;
581
+ requests: number;
582
+ }[]>;
583
+ /** Manual-adjustment audit log for this bucket. */
584
+ adjustments: (ctx: RunQueryCtx, args: {
585
+ userId: string;
586
+ } & {
587
+ limit?: number;
588
+ }) => Promise<{
589
+ _id: string;
590
+ _creationTime: number;
591
+ tokens?: number | undefined;
592
+ reason?: string | undefined;
593
+ dimension: string;
594
+ value: string;
595
+ deltaNanos: number;
596
+ }[]>;
597
+ /** Delete the bucket (for "user", also its request rows). */
229
598
  delete: (ctx: RunMutationCtx, args: {
230
599
  userId: string;
231
600
  }) => Promise<{
@@ -233,49 +602,143 @@ export declare class AIBudget {
233
602
  done: boolean;
234
603
  }>;
235
604
  };
236
- /** Per-action (per-feature) budgets. */
605
+ /** Per-action (per-feature) budgets — sugar over the "action" dimension. */
237
606
  get actions(): {
607
+ /** All buckets in this dimension. */
238
608
  list: (ctx: RunQueryCtx) => Promise<{
239
609
  spendTodayNanos: number;
610
+ spendThisMonthNanos: number;
240
611
  _id: string;
241
612
  _creationTime: number;
613
+ requestsPerMinute?: number | undefined;
614
+ maxConcurrent?: number | undefined;
242
615
  dailySpendLimitNanos?: number | undefined;
616
+ monthlySpendLimitNanos?: number | undefined;
243
617
  lifetimeSpendLimitNanos?: number | undefined;
244
618
  dailyTokenLimit?: number | undefined;
619
+ monthlyTokenLimit?: number | undefined;
245
620
  lifetimeTokenLimit?: number | undefined;
621
+ blocked?: boolean | undefined;
622
+ warnAtPct?: number | undefined;
246
623
  enforcement?: "hard" | "soft" | undefined;
247
624
  dailyBumpNanos?: number | undefined;
625
+ monthlyBumpNanos?: number | undefined;
248
626
  lifetimeBumpNanos?: number | undefined;
249
627
  bumpDayStamp?: string | undefined;
628
+ bumpMonthStamp?: string | undefined;
250
629
  tokensToday?: number | undefined;
630
+ monthStamp?: string | undefined;
631
+ tokensThisMonth?: number | undefined;
251
632
  reservedTodayNanos?: number | undefined;
633
+ reservedMonthNanos?: number | undefined;
252
634
  reservedTotalNanos?: number | undefined;
253
635
  reservedTodayTokens?: number | undefined;
636
+ reservedMonthTokens?: number | undefined;
254
637
  reservedTotalTokens?: number | undefined;
255
638
  pendingCount?: number | undefined;
256
- disabled?: boolean | undefined;
639
+ dimension: string;
640
+ value: string;
257
641
  totalSpendNanos: number;
258
642
  totalRequests: number;
259
643
  totalTokens: number;
260
644
  dayStamp: string;
261
- name: string;
262
645
  }[]>;
646
+ /** One bucket's limits + spend (null if it has none yet). */
647
+ get: (ctx: RunQueryCtx, args: {
648
+ name: string;
649
+ }) => Promise<{
650
+ spendTodayNanos: number;
651
+ spendThisMonthNanos: number;
652
+ _id: string;
653
+ _creationTime: number;
654
+ requestsPerMinute?: number | undefined;
655
+ maxConcurrent?: number | undefined;
656
+ dailySpendLimitNanos?: number | undefined;
657
+ monthlySpendLimitNanos?: number | undefined;
658
+ lifetimeSpendLimitNanos?: number | undefined;
659
+ dailyTokenLimit?: number | undefined;
660
+ monthlyTokenLimit?: number | undefined;
661
+ lifetimeTokenLimit?: number | undefined;
662
+ blocked?: boolean | undefined;
663
+ warnAtPct?: number | undefined;
664
+ enforcement?: "hard" | "soft" | undefined;
665
+ dailyBumpNanos?: number | undefined;
666
+ monthlyBumpNanos?: number | undefined;
667
+ lifetimeBumpNanos?: number | undefined;
668
+ bumpDayStamp?: string | undefined;
669
+ bumpMonthStamp?: string | undefined;
670
+ tokensToday?: number | undefined;
671
+ monthStamp?: string | undefined;
672
+ tokensThisMonth?: number | undefined;
673
+ reservedTodayNanos?: number | undefined;
674
+ reservedMonthNanos?: number | undefined;
675
+ reservedTotalNanos?: number | undefined;
676
+ reservedTodayTokens?: number | undefined;
677
+ reservedMonthTokens?: number | undefined;
678
+ reservedTotalTokens?: number | undefined;
679
+ pendingCount?: number | undefined;
680
+ dimension: string;
681
+ value: string;
682
+ totalSpendNanos: number;
683
+ totalRequests: number;
684
+ totalTokens: number;
685
+ dayStamp: string;
686
+ } | null>;
263
687
  setLimits: (ctx: RunMutationCtx, args: {
264
688
  name: string;
265
- dailySpendLimitNanos?: number;
266
- lifetimeSpendLimitNanos?: number;
267
- dailyTokenLimit?: number;
268
- lifetimeTokenLimit?: number;
269
- enforcement?: "hard" | "soft";
270
- disabled?: boolean;
271
- }) => Promise<null>;
689
+ } & BucketLimits) => Promise<null>;
690
+ /** One-time "approve another $X" bump (daily/monthly reset with the window). */
272
691
  bump: (ctx: RunMutationCtx, args: {
273
692
  name: string;
274
- dailyNanos?: number;
275
- lifetimeNanos?: number;
693
+ } & BumpArgs) => Promise<null>;
694
+ /** Manually credit (negative) or debit (positive) this bucket. */
695
+ adjust: (ctx: RunMutationCtx, args: {
696
+ name: string;
697
+ } & {
698
+ deltaNanos: number;
699
+ tokens?: number;
700
+ reason?: string;
276
701
  }) => Promise<null>;
702
+ /** Durable spend history for this bucket (per day or per month). */
703
+ history: (ctx: RunQueryCtx, args: {
704
+ name: string;
705
+ } & {
706
+ period?: "day" | "month";
707
+ limit?: number;
708
+ }) => Promise<{
709
+ _id: string;
710
+ _creationTime: number;
711
+ dimension: string;
712
+ value: string;
713
+ period: "day" | "month";
714
+ stamp: string;
715
+ spendNanos: number;
716
+ tokens: number;
717
+ requests: number;
718
+ }[]>;
719
+ /** Manual-adjustment audit log for this bucket. */
720
+ adjustments: (ctx: RunQueryCtx, args: {
721
+ name: string;
722
+ } & {
723
+ limit?: number;
724
+ }) => Promise<{
725
+ _id: string;
726
+ _creationTime: number;
727
+ tokens?: number | undefined;
728
+ reason?: string | undefined;
729
+ dimension: string;
730
+ value: string;
731
+ deltaNanos: number;
732
+ }[]>;
733
+ /** Delete the bucket (for "user", also its request rows). */
734
+ delete: (ctx: RunMutationCtx, args: {
735
+ name: string;
736
+ }) => Promise<{
737
+ deletedThisBatch: number;
738
+ done: boolean;
739
+ }>;
277
740
  };
278
- /** The deployment-wide budget and retention config. */
741
+ /** The deployment-wide budget, alerts, and retention config. */
279
742
  get global(): {
280
743
  /** Limits + spend today/total. */
281
744
  status: (ctx: RunQueryCtx) => Promise<{
@@ -284,6 +747,8 @@ export declare class AIBudget {
284
747
  enforcement: "hard" | "soft";
285
748
  spentTodayNanos: number;
286
749
  spentTotalNanos: number;
750
+ retentionMs: number | null;
751
+ defaultWarnAtPct: number | null;
287
752
  }>;
288
753
  /** A killswitch spend cap across all users/actions (enforced approximately). */
289
754
  setLimits: (ctx: RunMutationCtx, args: {
@@ -295,6 +760,10 @@ export declare class AIBudget {
295
760
  dailyNanos?: number;
296
761
  lifetimeNanos?: number;
297
762
  }) => Promise<null>;
763
+ /** Default approaching-limit alert threshold (fraction of a cap, e.g. 0.8). */
764
+ setAlertDefaults: (ctx: RunMutationCtx, args: {
765
+ warnAtPct?: number;
766
+ }) => Promise<null>;
298
767
  /** Request-row retention window in ms (default 1h; 0 disables). */
299
768
  setRetention: (ctx: RunMutationCtx, args: {
300
769
  retentionMs: number;
@@ -312,12 +781,13 @@ export declare class AIBudget {
312
781
  models: string[];
313
782
  }) => Promise<null>;
314
783
  };
315
- /** Per-model prices (cents per million tokens). */
784
+ /** Per-model prices (nanodollars per million tokens) + server-tool fees. */
316
785
  get prices(): {
317
786
  list: (ctx: RunQueryCtx) => Promise<{
318
787
  [x: string]: {
319
788
  input: number;
320
789
  output: number;
790
+ cached?: number | undefined;
321
791
  overridden: boolean;
322
792
  };
323
793
  }>;
@@ -325,8 +795,45 @@ export declare class AIBudget {
325
795
  model: string;
326
796
  inputNanosPerMTok: number;
327
797
  outputNanosPerMTok: number;
798
+ /** Cache-read rate; defaults to a discount off input if omitted. */
799
+ cachedNanosPerMTok?: number;
800
+ }) => Promise<null>;
801
+ /** Per-call fees for provider server tools (web search, etc.). */
802
+ listServerTools: (ctx: RunQueryCtx) => Promise<{
803
+ [x: string]: number;
804
+ }>;
805
+ /** Set a server-tool's per-call price, e.g. { tool: "web_search", nanosPerCall }. */
806
+ setServerTool: (ctx: RunMutationCtx, args: {
807
+ tool: string;
808
+ nanosPerCall: number;
328
809
  }) => Promise<null>;
329
810
  };
811
+ /**
812
+ * Mount the built-in admin dashboard on your app's HTTP router with one call.
813
+ * Serves a self-contained HTML dashboard (buckets, requests, usage history,
814
+ * settings) plus a small JSON API, all backed by the component — no extra
815
+ * queries to write.
816
+ *
817
+ * // convex/http.ts
818
+ * import { httpRouter } from "convex/server";
819
+ * const http = httpRouter();
820
+ * ai.registerRoutes(http, { authorize: async (ctx) =>
821
+ * (await ctx.auth.getUserIdentity())?.role === "admin" });
822
+ * export default http;
823
+ *
824
+ * It then lives at `https://<deployment>.convex.site/aibudget`.
825
+ *
826
+ * SECURITY: the endpoint is public on the internet. You MUST gate it — either
827
+ * pass `authorize` (recommended: check the caller is a deployment admin) or
828
+ * set the `AI_BUDGET_DASHBOARD_TOKEN` env var (a bearer token / `?token=`).
829
+ * With neither, every route returns 401.
830
+ */
831
+ registerRoutes(http: HttpRouter, opts?: {
832
+ /** Mount path (default "/aibudget"). */
833
+ path?: string;
834
+ /** Return true to allow the request. Runs on the HTML page and every API call. */
835
+ authorize?: (ctx: any, request: Request) => boolean | Promise<boolean>;
836
+ }): void;
330
837
  }
331
838
  /** @deprecated Renamed to `AIBudget`. */
332
839
  export declare const WorryFreeAI: typeof AIBudget;