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

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.
@@ -5,8 +5,15 @@ export declare const vMessage: import("convex/values").VObject<{
5
5
  role: import("convex/values").VString<string, "required">;
6
6
  content: import("convex/values").VString<string, "required">;
7
7
  }, "required", "role" | "content">;
8
+ export declare const vTag: import("convex/values").VObject<{
9
+ dimension: string;
10
+ value: string;
11
+ }, {
12
+ dimension: import("convex/values").VString<string, "required">;
13
+ value: import("convex/values").VString<string, "required">;
14
+ }, "required", "dimension" | "value">;
8
15
  declare const _default: import("convex/server").SchemaDefinition<{
9
- users: import("convex/server").TableDefinition<import("convex/values").VObject<{
16
+ buckets: import("convex/server").TableDefinition<import("convex/values").VObject<{
10
17
  requestsPerMinute?: number | undefined;
11
18
  dailySpendLimitNanos?: number | undefined;
12
19
  lifetimeSpendLimitNanos?: number | undefined;
@@ -23,14 +30,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
23
30
  reservedTodayTokens?: number | undefined;
24
31
  reservedTotalTokens?: number | undefined;
25
32
  pendingCount?: number | undefined;
26
- userId: string;
33
+ dimension: string;
34
+ value: string;
27
35
  totalSpendNanos: number;
28
36
  totalRequests: number;
29
37
  totalTokens: number;
30
38
  dayStamp: string;
31
39
  spendTodayNanos: number;
32
40
  }, {
33
- userId: import("convex/values").VString<string, "required">;
41
+ dimension: import("convex/values").VString<string, "required">;
42
+ value: import("convex/values").VString<string, "required">;
34
43
  requestsPerMinute: import("convex/values").VFloat64<number | undefined, "optional">;
35
44
  dailySpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
36
45
  lifetimeSpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
@@ -52,58 +61,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
52
61
  reservedTodayTokens: import("convex/values").VFloat64<number | undefined, "optional">;
53
62
  reservedTotalTokens: import("convex/values").VFloat64<number | undefined, "optional">;
54
63
  pendingCount: import("convex/values").VFloat64<number | undefined, "optional">;
55
- }, "required", "userId" | "requestsPerMinute" | "dailySpendLimitNanos" | "lifetimeSpendLimitNanos" | "dailyTokenLimit" | "lifetimeTokenLimit" | "blocked" | "enforcement" | "dailyBumpNanos" | "lifetimeBumpNanos" | "bumpDayStamp" | "totalSpendNanos" | "totalRequests" | "totalTokens" | "dayStamp" | "spendTodayNanos" | "tokensToday" | "reservedTodayNanos" | "reservedTotalNanos" | "reservedTodayTokens" | "reservedTotalTokens" | "pendingCount">, {
56
- userId: ["userId", "_creationTime"];
57
- }, {}, {}>;
58
- actions: import("convex/server").TableDefinition<import("convex/values").VObject<{
59
- dailySpendLimitNanos?: number | undefined;
60
- lifetimeSpendLimitNanos?: number | undefined;
61
- dailyTokenLimit?: number | undefined;
62
- lifetimeTokenLimit?: number | undefined;
63
- enforcement?: "hard" | "soft" | undefined;
64
- dailyBumpNanos?: number | undefined;
65
- lifetimeBumpNanos?: number | undefined;
66
- bumpDayStamp?: string | undefined;
67
- tokensToday?: number | undefined;
68
- reservedTodayNanos?: number | undefined;
69
- reservedTotalNanos?: number | undefined;
70
- reservedTodayTokens?: number | undefined;
71
- reservedTotalTokens?: number | undefined;
72
- pendingCount?: number | undefined;
73
- disabled?: boolean | undefined;
74
- totalSpendNanos: number;
75
- totalRequests: number;
76
- totalTokens: number;
77
- dayStamp: string;
78
- spendTodayNanos: number;
79
- name: string;
80
- }, {
81
- name: import("convex/values").VString<string, "required">;
82
- dailySpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
83
- lifetimeSpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
84
- dailyTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
85
- lifetimeTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
86
- disabled: import("convex/values").VBoolean<boolean | undefined, "optional">;
87
- enforcement: import("convex/values").VUnion<"hard" | "soft" | undefined, [import("convex/values").VLiteral<"hard", "required">, import("convex/values").VLiteral<"soft", "required">], "optional", never>;
88
- dailyBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
89
- lifetimeBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
90
- bumpDayStamp: import("convex/values").VString<string | undefined, "optional">;
91
- totalSpendNanos: import("convex/values").VFloat64<number, "required">;
92
- totalRequests: import("convex/values").VFloat64<number, "required">;
93
- totalTokens: import("convex/values").VFloat64<number, "required">;
94
- dayStamp: import("convex/values").VString<string, "required">;
95
- spendTodayNanos: import("convex/values").VFloat64<number, "required">;
96
- tokensToday: import("convex/values").VFloat64<number | undefined, "optional">;
97
- reservedTodayNanos: import("convex/values").VFloat64<number | undefined, "optional">;
98
- reservedTotalNanos: import("convex/values").VFloat64<number | undefined, "optional">;
99
- reservedTodayTokens: import("convex/values").VFloat64<number | undefined, "optional">;
100
- reservedTotalTokens: import("convex/values").VFloat64<number | undefined, "optional">;
101
- pendingCount: import("convex/values").VFloat64<number | undefined, "optional">;
102
- }, "required", "dailySpendLimitNanos" | "lifetimeSpendLimitNanos" | "dailyTokenLimit" | "lifetimeTokenLimit" | "enforcement" | "dailyBumpNanos" | "lifetimeBumpNanos" | "bumpDayStamp" | "totalSpendNanos" | "totalRequests" | "totalTokens" | "dayStamp" | "spendTodayNanos" | "tokensToday" | "reservedTodayNanos" | "reservedTotalNanos" | "reservedTodayTokens" | "reservedTotalTokens" | "pendingCount" | "name" | "disabled">, {
103
- name: ["name", "_creationTime"];
64
+ }, "required", "dimension" | "value" | "requestsPerMinute" | "dailySpendLimitNanos" | "lifetimeSpendLimitNanos" | "dailyTokenLimit" | "lifetimeTokenLimit" | "blocked" | "enforcement" | "dailyBumpNanos" | "lifetimeBumpNanos" | "bumpDayStamp" | "totalSpendNanos" | "totalRequests" | "totalTokens" | "dayStamp" | "spendTodayNanos" | "tokensToday" | "reservedTodayNanos" | "reservedTotalNanos" | "reservedTodayTokens" | "reservedTotalTokens" | "pendingCount">, {
65
+ dim_value: ["dimension", "value", "_creationTime"];
66
+ dimension: ["dimension", "_creationTime"];
104
67
  }, {}, {}>;
105
68
  requests: import("convex/server").TableDefinition<import("convex/values").VObject<{
106
69
  actionName?: string | undefined;
70
+ tags?: {
71
+ dimension: string;
72
+ value: string;
73
+ }[] | undefined;
107
74
  estimatedNanos?: number | undefined;
108
75
  estimatedTokens?: number | undefined;
109
76
  unpricedModel?: boolean | undefined;
@@ -127,6 +94,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
127
94
  }, {
128
95
  userId: import("convex/values").VString<string, "required">;
129
96
  actionName: import("convex/values").VString<string | undefined, "optional">;
97
+ tags: import("convex/values").VArray<{
98
+ dimension: string;
99
+ value: string;
100
+ }[] | undefined, import("convex/values").VObject<{
101
+ dimension: string;
102
+ value: string;
103
+ }, {
104
+ dimension: import("convex/values").VString<string, "required">;
105
+ value: import("convex/values").VString<string, "required">;
106
+ }, "required", "dimension" | "value">, "optional">;
130
107
  model: import("convex/values").VString<string, "required">;
131
108
  estimatedNanos: import("convex/values").VFloat64<number | undefined, "optional">;
132
109
  estimatedTokens: import("convex/values").VFloat64<number | undefined, "optional">;
@@ -152,7 +129,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
152
129
  costNanos: import("convex/values").VFloat64<number | undefined, "optional">;
153
130
  latencyMs: import("convex/values").VFloat64<number | undefined, "optional">;
154
131
  rerunOf: import("convex/values").VId<import("convex/values").GenericId<"requests"> | undefined, "optional">;
155
- }, "required", "userId" | "actionName" | "model" | "estimatedNanos" | "estimatedTokens" | "unpricedModel" | "overBudget" | "settled" | "messages" | "status" | "error" | "responseText" | "promptTokens" | "completionTokens" | "cachedTokens" | "costNanos" | "latencyMs" | "rerunOf">, {
132
+ }, "required", "userId" | "actionName" | "tags" | "model" | "estimatedNanos" | "estimatedTokens" | "unpricedModel" | "overBudget" | "settled" | "messages" | "status" | "error" | "responseText" | "promptTokens" | "completionTokens" | "cachedTokens" | "costNanos" | "latencyMs" | "rerunOf">, {
156
133
  userId: ["userId", "_creationTime"];
157
134
  status: ["status", "_creationTime"];
158
135
  rerunOf: ["rerunOf", "_creationTime"];
@@ -4,20 +4,28 @@ export const vMessage = v.object({
4
4
  role: v.string(), // "system" | "user" | "assistant" | "tool"
5
5
  content: v.string(),
6
6
  });
7
+ // One attribution tag: a (dimension, value) pair, e.g. {dimension:"user",
8
+ // value:"alice"} or {dimension:"customer", value:"acme"}. `user` and `action`
9
+ // are built-in dimensions; apps can add any others (team, project, env, …).
10
+ export const vTag = v.object({ dimension: v.string(), value: v.string() });
7
11
  export default defineSchema({
8
- users: defineTable({
9
- userId: v.string(), // app-provided stable key (your user id)
12
+ // A budget holder, keyed by (dimension, value). Unifies what used to be the
13
+ // `users` and `actions` tables those are just the "user" and "action"
14
+ // dimensions now. Any tag a request carries can have its own budget here.
15
+ buckets: defineTable({
16
+ dimension: v.string(),
17
+ value: v.string(),
10
18
  // limits (all optional — unlimited by default)
11
- requestsPerMinute: v.optional(v.number()),
19
+ requestsPerMinute: v.optional(v.number()), // enforced on the "user" dimension
12
20
  dailySpendLimitNanos: v.optional(v.number()),
13
21
  lifetimeSpendLimitNanos: v.optional(v.number()),
14
22
  dailyTokenLimit: v.optional(v.number()),
15
23
  lifetimeTokenLimit: v.optional(v.number()),
16
- blocked: v.optional(v.boolean()),
24
+ blocked: v.optional(v.boolean()), // hard block (was `blocked`/`disabled`)
17
25
  // "hard" (default): exceeding a budget blocks. "soft": warn but allow.
18
26
  enforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
19
- // one-time bumps ("approve another $X") added on top of the cap. Daily bump
20
- // is scoped to bumpDayStamp (resets with the day); lifetime bump is permanent.
27
+ // one-time bumps ("approve another $X"). Daily bump is scoped to bumpDayStamp
28
+ // (resets with the day); lifetime bump is permanent.
21
29
  dailyBumpNanos: v.optional(v.number()),
22
30
  lifetimeBumpNanos: v.optional(v.number()),
23
31
  bumpDayStamp: v.optional(v.string()),
@@ -26,43 +34,24 @@ export default defineSchema({
26
34
  totalRequests: v.number(),
27
35
  totalTokens: v.number(),
28
36
  // daily window
29
- dayStamp: v.string(), // e.g. "2026-08-27" (UTC)
30
- spendTodayNanos: v.number(),
31
- tokensToday: v.optional(v.number()),
32
- // in-flight reservations (pessimistic holds; released on settle/expiry)
33
- reservedTodayNanos: v.optional(v.number()),
34
- reservedTotalNanos: v.optional(v.number()),
35
- reservedTodayTokens: v.optional(v.number()),
36
- reservedTotalTokens: v.optional(v.number()),
37
- pendingCount: v.optional(v.number()),
38
- }).index("userId", ["userId"]),
39
- // per-action-name budgets and running totals (e.g. "chat", "summarize")
40
- actions: defineTable({
41
- name: v.string(),
42
- dailySpendLimitNanos: v.optional(v.number()),
43
- lifetimeSpendLimitNanos: v.optional(v.number()),
44
- dailyTokenLimit: v.optional(v.number()),
45
- lifetimeTokenLimit: v.optional(v.number()),
46
- disabled: v.optional(v.boolean()),
47
- enforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
48
- dailyBumpNanos: v.optional(v.number()),
49
- lifetimeBumpNanos: v.optional(v.number()),
50
- bumpDayStamp: v.optional(v.string()),
51
- totalSpendNanos: v.number(),
52
- totalRequests: v.number(),
53
- totalTokens: v.number(),
54
37
  dayStamp: v.string(),
55
38
  spendTodayNanos: v.number(),
56
39
  tokensToday: v.optional(v.number()),
40
+ // in-flight reservations (pessimistic holds; released on settle/expiry)
57
41
  reservedTodayNanos: v.optional(v.number()),
58
42
  reservedTotalNanos: v.optional(v.number()),
59
43
  reservedTodayTokens: v.optional(v.number()),
60
44
  reservedTotalTokens: v.optional(v.number()),
61
45
  pendingCount: v.optional(v.number()),
62
- }).index("name", ["name"]),
46
+ })
47
+ .index("dim_value", ["dimension", "value"])
48
+ .index("dimension", ["dimension"]),
63
49
  requests: defineTable({
50
+ // `user` and `action` stay first-class + indexed (the hot-path filters and
51
+ // rate limiting); the full attribution incl. extra tags lives in `tags`.
64
52
  userId: v.string(),
65
53
  actionName: v.optional(v.string()),
54
+ tags: v.optional(v.array(vTag)),
66
55
  model: v.string(),
67
56
  // pessimistic holds placed at start; reconciled to actual on settle
68
57
  estimatedNanos: v.optional(v.number()),
@@ -105,12 +94,12 @@ export default defineSchema({
105
94
  // "denylist": any model except the listed ones.
106
95
  modelMode: v.optional(v.union(v.literal("open"), v.literal("allowlist"), v.literal("denylist"))),
107
96
  models: v.optional(v.array(v.string())),
108
- // Deployment-wide ("global") spend cap across ALL users and actions. The
109
- // running totals live in a sharded counter (high write throughput); only
110
- // the limit config lives here. The cap is enforced approximately — the
111
- // sharded total is read without a reservation, so under heavy concurrency
112
- // it can overshoot by a bounded amount. Right for a global killswitch;
113
- // per-user/per-action caps stay exact via reserve/settle.
97
+ // Deployment-wide ("global") spend cap across ALL requests. Running totals
98
+ // live in a sharded counter (high write throughput) since every request
99
+ // touches it; only the limit config lives here. Enforced approximately —
100
+ // the sharded total is read without a reservation, so under heavy
101
+ // concurrency it can overshoot by a bounded amount. Right for a global
102
+ // killswitch; per-bucket caps stay exact via reserve/settle.
114
103
  globalDailySpendLimitNanos: v.optional(v.number()),
115
104
  globalLifetimeSpendLimitNanos: v.optional(v.number()),
116
105
  globalEnforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "email": "support@convex.dev",
8
8
  "url": "https://github.com/get-convex/ai-budget/issues"
9
9
  },
10
- "version": "0.0.2-alpha.0",
10
+ "version": "0.0.2-alpha.4",
11
11
  "license": "Apache-2.0",
12
12
  "type": "module",
13
13
  "keywords": [
@@ -64,10 +64,10 @@
64
64
  "convex": "^1.45.0"
65
65
  },
66
66
  "devDependencies": {
67
- "@edge-runtime/vm": "^5.0.0",
68
- "convex": "^1.45.0",
69
- "convex-test": "^0.0.38",
67
+ "@edge-runtime/vm": "5.0.0",
68
+ "convex": "1.45.0",
69
+ "convex-test": "0.0.56",
70
70
  "typescript": "^5.7.0",
71
- "vitest": "^4.0.0"
71
+ "vitest": "4.1.11"
72
72
  }
73
73
  }
@@ -41,10 +41,19 @@ export type AIBudgetApi = UseApi<typeof api>;
41
41
  /** @deprecated use AIBudgetApi */
42
42
  export type AIGatewayApi = AIBudgetApi;
43
43
 
44
+ /**
45
+ * One attribution tag: a (dimension, value) pair, e.g. {dimension:"customer",
46
+ * value:"acme"}. `user` and `action` are built-in dimensions (set via
47
+ * userId/action); use tags for anything else — team, project, tenant, env, ….
48
+ * Any tagged bucket can carry its own budget (see `ai.tag(dimension)`).
49
+ */
50
+ export type Tag = { dimension: string; value: string };
51
+
44
52
  /** Fired when a request is admitted over a *soft* limit. */
45
53
  export type SoftLimitInfo = {
46
54
  userId: string;
47
55
  action?: string;
56
+ tags?: Tag[];
48
57
  requestId: string;
49
58
  warnings: string[];
50
59
  };
@@ -213,6 +222,8 @@ export class AIBudget {
213
222
  rerunOf?: string;
214
223
  /** Attribute spend to this action name. Defaults to the calling Convex action. */
215
224
  action?: string;
225
+ /** Extra attribution dimensions to bill/limit (team, customer, env, …). */
226
+ tags?: Tag[];
216
227
  } = {}
217
228
  ): Promise<ChatResult> {
218
229
  const model = args.model ?? this.defaultModel;
@@ -223,6 +234,7 @@ export class AIBudget {
223
234
  const started = await ctx.runMutation(this.component.lib.startRequest, {
224
235
  userId,
225
236
  actionName,
237
+ tags: args.tags,
226
238
  model,
227
239
  messages,
228
240
  rerunOf: args.rerunOf as any,
@@ -236,7 +248,13 @@ export class AIBudget {
236
248
  }
237
249
  const requestId = started.requestId;
238
250
  const warnings = started.warnings;
239
- await this.fireSoftLimit({ userId, action: actionName, requestId, warnings });
251
+ await this.fireSoftLimit({
252
+ userId,
253
+ action: actionName,
254
+ tags: args.tags,
255
+ requestId,
256
+ warnings,
257
+ });
240
258
  const start = Date.now();
241
259
  try {
242
260
  // The full chain (incl. system) is stored on the request for audit/replay,
@@ -281,7 +299,13 @@ export class AIBudget {
281
299
  */
282
300
  languageModel(
283
301
  ctx: RunMutationCtx,
284
- opts: { userId?: string; model?: string; action?: string } = {}
302
+ opts: {
303
+ userId?: string;
304
+ model?: string;
305
+ action?: string;
306
+ /** Extra attribution dimensions to bill/limit (team, customer, env, …). */
307
+ tags?: Tag[];
308
+ } = {}
285
309
  ): LanguageModel {
286
310
  const modelId = opts.model ?? this.defaultModel;
287
311
  const component = this.component;
@@ -293,6 +317,7 @@ export class AIBudget {
293
317
  const started = await ctx.runMutation(component.lib.startRequest, {
294
318
  userId,
295
319
  actionName,
320
+ tags: opts.tags,
296
321
  model: modelId,
297
322
  messages: simplifyPrompt(params.prompt),
298
323
  });
@@ -306,6 +331,7 @@ export class AIBudget {
306
331
  await fireSoftLimit({
307
332
  userId,
308
333
  action: actionName,
334
+ tags: opts.tags,
309
335
  requestId: started.requestId,
310
336
  warnings: started.warnings,
311
337
  });
@@ -409,6 +435,7 @@ export class AIBudget {
409
435
  messages: args.messages ?? original.messages,
410
436
  rerunOf: args.requestId,
411
437
  action: original.actionName,
438
+ tags: original.tags,
412
439
  });
413
440
  }
414
441
 
@@ -431,11 +458,55 @@ export class AIBudget {
431
458
  };
432
459
  }
433
460
 
434
- /** Per-user budgets and controls. */
461
+ /**
462
+ * Budgets and controls for an arbitrary attribution dimension — the
463
+ * generalization of `users`/`actions`. Give it any dimension name (team,
464
+ * project, tenant, customer, env, feature, …) and set caps per value:
465
+ *
466
+ * ai.tag("customer").setLimits(ctx, { value: "acme", dailySpendLimitNanos });
467
+ * ai.tag("customer").list(ctx);
468
+ *
469
+ * Attribute a call to it by passing `tags` to `chat`/`languageModel`.
470
+ */
471
+ tag(dimension: string) {
472
+ const c = this.component;
473
+ return {
474
+ /** All buckets in this dimension. */
475
+ list: (ctx: RunQueryCtx) =>
476
+ ctx.runQuery(c.lib.listBuckets, { dimension }),
477
+ /** One bucket's limits + spend (null if it has none yet). */
478
+ get: (ctx: RunQueryCtx, args: { value: string }) =>
479
+ ctx.runQuery(c.lib.getBucket, { dimension, value: args.value }),
480
+ setLimits: (
481
+ ctx: RunMutationCtx,
482
+ args: {
483
+ value: string;
484
+ requestsPerMinute?: number;
485
+ dailySpendLimitNanos?: number;
486
+ lifetimeSpendLimitNanos?: number;
487
+ dailyTokenLimit?: number;
488
+ lifetimeTokenLimit?: number;
489
+ enforcement?: "hard" | "soft";
490
+ blocked?: boolean;
491
+ }
492
+ ) => ctx.runMutation(c.lib.setBucketLimits, { dimension, ...args }),
493
+ /** One-time "approve another $X" bump (daily is today-only). */
494
+ bump: (
495
+ ctx: RunMutationCtx,
496
+ args: { value: string; dailyNanos?: number; lifetimeNanos?: number }
497
+ ) => ctx.runMutation(c.lib.bumpBucket, { dimension, ...args }),
498
+ /** Delete the bucket (for "user", also its request rows). */
499
+ delete: (ctx: RunMutationCtx, args: { value: string }) =>
500
+ ctx.runMutation(c.lib.deleteBucket, { dimension, value: args.value }),
501
+ };
502
+ }
503
+
504
+ /** Per-user budgets and controls — sugar over the "user" dimension. */
435
505
  get users() {
436
506
  const c = this.component;
437
507
  return {
438
- list: (ctx: RunQueryCtx) => ctx.runQuery(c.lib.listUsers, {}),
508
+ list: (ctx: RunQueryCtx) =>
509
+ ctx.runQuery(c.lib.listBuckets, { dimension: "user" }),
439
510
  setLimits: (
440
511
  ctx: RunMutationCtx,
441
512
  args: {
@@ -448,23 +519,40 @@ export class AIBudget {
448
519
  enforcement?: "hard" | "soft";
449
520
  blocked?: boolean;
450
521
  }
451
- ) => ctx.runMutation(c.lib.setLimits, args),
522
+ ) => {
523
+ const { userId, ...limits } = args;
524
+ return ctx.runMutation(c.lib.setBucketLimits, {
525
+ dimension: "user",
526
+ value: userId,
527
+ ...limits,
528
+ });
529
+ },
452
530
  /** One-time "approve another $X" bump (daily is today-only). */
453
531
  bump: (
454
532
  ctx: RunMutationCtx,
455
533
  args: { userId: string; dailyNanos?: number; lifetimeNanos?: number }
456
- ) => ctx.runMutation(c.lib.bumpUser, args),
534
+ ) =>
535
+ ctx.runMutation(c.lib.bumpBucket, {
536
+ dimension: "user",
537
+ value: args.userId,
538
+ dailyNanos: args.dailyNanos,
539
+ lifetimeNanos: args.lifetimeNanos,
540
+ }),
457
541
  /** Delete a user and all their request rows. */
458
542
  delete: (ctx: RunMutationCtx, args: { userId: string }) =>
459
- ctx.runMutation(c.lib.deleteUser, args),
543
+ ctx.runMutation(c.lib.deleteBucket, {
544
+ dimension: "user",
545
+ value: args.userId,
546
+ }),
460
547
  };
461
548
  }
462
549
 
463
- /** Per-action (per-feature) budgets. */
550
+ /** Per-action (per-feature) budgets — sugar over the "action" dimension. */
464
551
  get actions() {
465
552
  const c = this.component;
466
553
  return {
467
- list: (ctx: RunQueryCtx) => ctx.runQuery(c.lib.listActions, {}),
554
+ list: (ctx: RunQueryCtx) =>
555
+ ctx.runQuery(c.lib.listBuckets, { dimension: "action" }),
468
556
  setLimits: (
469
557
  ctx: RunMutationCtx,
470
558
  args: {
@@ -474,13 +562,27 @@ export class AIBudget {
474
562
  dailyTokenLimit?: number;
475
563
  lifetimeTokenLimit?: number;
476
564
  enforcement?: "hard" | "soft";
477
- disabled?: boolean;
565
+ /** Hard-block this action (was `disabled`). */
566
+ blocked?: boolean;
478
567
  }
479
- ) => ctx.runMutation(c.lib.setActionLimits, args),
568
+ ) => {
569
+ const { name, ...limits } = args;
570
+ return ctx.runMutation(c.lib.setBucketLimits, {
571
+ dimension: "action",
572
+ value: name,
573
+ ...limits,
574
+ });
575
+ },
480
576
  bump: (
481
577
  ctx: RunMutationCtx,
482
578
  args: { name: string; dailyNanos?: number; lifetimeNanos?: number }
483
- ) => ctx.runMutation(c.lib.bumpAction, args),
579
+ ) =>
580
+ ctx.runMutation(c.lib.bumpBucket, {
581
+ dimension: "action",
582
+ value: args.name,
583
+ dailyNanos: args.dailyNanos,
584
+ lifetimeNanos: args.lifetimeNanos,
585
+ }),
484
586
  };
485
587
  }
486
588
 
@@ -24,10 +24,15 @@ import type { FunctionReference } from "convex/server";
24
24
  export type ComponentApi<Name extends string | undefined = string | undefined> =
25
25
  {
26
26
  lib: {
27
- bumpAction: FunctionReference<
27
+ bumpBucket: FunctionReference<
28
28
  "mutation",
29
29
  "internal",
30
- { dailyNanos?: number; lifetimeNanos?: number; name: string },
30
+ {
31
+ dailyNanos?: number;
32
+ dimension: string;
33
+ lifetimeNanos?: number;
34
+ value: string;
35
+ },
31
36
  null,
32
37
  Name
33
38
  >;
@@ -38,17 +43,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
38
43
  null,
39
44
  Name
40
45
  >;
41
- bumpUser: FunctionReference<
46
+ deleteBucket: FunctionReference<
42
47
  "mutation",
43
48
  "internal",
44
- { dailyNanos?: number; lifetimeNanos?: number; userId: string },
45
- null,
46
- Name
47
- >;
48
- deleteUser: FunctionReference<
49
- "mutation",
50
- "internal",
51
- { userId: string },
49
+ { dimension: string; value: string },
52
50
  { deletedThisBatch: number; done: boolean },
53
51
  Name
54
52
  >;
@@ -67,6 +65,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
67
65
  { costNanos: number },
68
66
  Name
69
67
  >;
68
+ getBucket: FunctionReference<
69
+ "query",
70
+ "internal",
71
+ { dimension: string; value: string },
72
+ any,
73
+ Name
74
+ >;
70
75
  getGlobalStatus: FunctionReference<
71
76
  "query",
72
77
  "internal",
@@ -101,7 +106,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
101
106
  any,
102
107
  Name
103
108
  >;
104
- listActions: FunctionReference<"query", "internal", {}, any, Name>;
109
+ listBuckets: FunctionReference<
110
+ "query",
111
+ "internal",
112
+ { dimension?: string },
113
+ any,
114
+ Name
115
+ >;
105
116
  listPrices: FunctionReference<"query", "internal", {}, any, Name>;
106
117
  listRequests: FunctionReference<
107
118
  "query",
@@ -110,18 +121,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
110
121
  any,
111
122
  Name
112
123
  >;
113
- listUsers: FunctionReference<"query", "internal", {}, any, Name>;
114
- setActionLimits: FunctionReference<
124
+ setBucketLimits: FunctionReference<
115
125
  "mutation",
116
126
  "internal",
117
127
  {
128
+ blocked?: boolean;
118
129
  dailySpendLimitNanos?: number;
119
130
  dailyTokenLimit?: number;
120
- disabled?: boolean;
131
+ dimension: string;
121
132
  enforcement?: "hard" | "soft";
122
133
  lifetimeSpendLimitNanos?: number;
123
134
  lifetimeTokenLimit?: number;
124
- name: string;
135
+ requestsPerMinute?: number;
136
+ value: string;
125
137
  },
126
138
  null,
127
139
  Name
@@ -137,22 +149,6 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
137
149
  null,
138
150
  Name
139
151
  >;
140
- setLimits: FunctionReference<
141
- "mutation",
142
- "internal",
143
- {
144
- blocked?: boolean;
145
- dailySpendLimitNanos?: number;
146
- dailyTokenLimit?: number;
147
- enforcement?: "hard" | "soft";
148
- lifetimeSpendLimitNanos?: number;
149
- lifetimeTokenLimit?: number;
150
- requestsPerMinute?: number;
151
- userId: string;
152
- },
153
- null,
154
- Name
155
- >;
156
152
  setModelPolicy: FunctionReference<
157
153
  "mutation",
158
154
  "internal",
@@ -186,6 +182,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
186
182
  messages: Array<{ content: string; role: string }>;
187
183
  model: string;
188
184
  rerunOf?: string;
185
+ tags?: Array<{ dimension: string; value: string }>;
189
186
  userId: string;
190
187
  },
191
188
  | { allowed: true; requestId: string; warnings: Array<string> }