@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.
@@ -6,20 +6,29 @@ export const vMessage = v.object({
6
6
  content: v.string(),
7
7
  });
8
8
 
9
+ // One attribution tag: a (dimension, value) pair, e.g. {dimension:"user",
10
+ // value:"alice"} or {dimension:"customer", value:"acme"}. `user` and `action`
11
+ // are built-in dimensions; apps can add any others (team, project, env, …).
12
+ export const vTag = v.object({ dimension: v.string(), value: v.string() });
13
+
9
14
  export default defineSchema({
10
- users: defineTable({
11
- userId: v.string(), // app-provided stable key (your user id)
15
+ // A budget holder, keyed by (dimension, value). Unifies what used to be the
16
+ // `users` and `actions` tables those are just the "user" and "action"
17
+ // dimensions now. Any tag a request carries can have its own budget here.
18
+ buckets: defineTable({
19
+ dimension: v.string(),
20
+ value: v.string(),
12
21
  // limits (all optional — unlimited by default)
13
- requestsPerMinute: v.optional(v.number()),
22
+ requestsPerMinute: v.optional(v.number()), // enforced on the "user" dimension
14
23
  dailySpendLimitNanos: v.optional(v.number()),
15
24
  lifetimeSpendLimitNanos: v.optional(v.number()),
16
25
  dailyTokenLimit: v.optional(v.number()),
17
26
  lifetimeTokenLimit: v.optional(v.number()),
18
- blocked: v.optional(v.boolean()),
27
+ blocked: v.optional(v.boolean()), // hard block (was `blocked`/`disabled`)
19
28
  // "hard" (default): exceeding a budget blocks. "soft": warn but allow.
20
29
  enforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
21
- // one-time bumps ("approve another $X") added on top of the cap. Daily bump
22
- // is scoped to bumpDayStamp (resets with the day); lifetime bump is permanent.
30
+ // one-time bumps ("approve another $X"). Daily bump is scoped to bumpDayStamp
31
+ // (resets with the day); lifetime bump is permanent.
23
32
  dailyBumpNanos: v.optional(v.number()),
24
33
  lifetimeBumpNanos: v.optional(v.number()),
25
34
  bumpDayStamp: v.optional(v.string()),
@@ -28,45 +37,25 @@ export default defineSchema({
28
37
  totalRequests: v.number(),
29
38
  totalTokens: v.number(),
30
39
  // daily window
31
- dayStamp: v.string(), // e.g. "2026-08-27" (UTC)
32
- spendTodayNanos: v.number(),
33
- tokensToday: v.optional(v.number()),
34
- // in-flight reservations (pessimistic holds; released on settle/expiry)
35
- reservedTodayNanos: v.optional(v.number()),
36
- reservedTotalNanos: v.optional(v.number()),
37
- reservedTodayTokens: v.optional(v.number()),
38
- reservedTotalTokens: v.optional(v.number()),
39
- pendingCount: v.optional(v.number()),
40
- }).index("userId", ["userId"]),
41
-
42
- // per-action-name budgets and running totals (e.g. "chat", "summarize")
43
- actions: defineTable({
44
- name: v.string(),
45
- dailySpendLimitNanos: v.optional(v.number()),
46
- lifetimeSpendLimitNanos: v.optional(v.number()),
47
- dailyTokenLimit: v.optional(v.number()),
48
- lifetimeTokenLimit: v.optional(v.number()),
49
- disabled: v.optional(v.boolean()),
50
- enforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
51
- dailyBumpNanos: v.optional(v.number()),
52
- lifetimeBumpNanos: v.optional(v.number()),
53
- bumpDayStamp: v.optional(v.string()),
54
- totalSpendNanos: v.number(),
55
- totalRequests: v.number(),
56
- totalTokens: v.number(),
57
40
  dayStamp: v.string(),
58
41
  spendTodayNanos: v.number(),
59
42
  tokensToday: v.optional(v.number()),
43
+ // in-flight reservations (pessimistic holds; released on settle/expiry)
60
44
  reservedTodayNanos: v.optional(v.number()),
61
45
  reservedTotalNanos: v.optional(v.number()),
62
46
  reservedTodayTokens: v.optional(v.number()),
63
47
  reservedTotalTokens: v.optional(v.number()),
64
48
  pendingCount: v.optional(v.number()),
65
- }).index("name", ["name"]),
49
+ })
50
+ .index("dim_value", ["dimension", "value"])
51
+ .index("dimension", ["dimension"]),
66
52
 
67
53
  requests: defineTable({
54
+ // `user` and `action` stay first-class + indexed (the hot-path filters and
55
+ // rate limiting); the full attribution incl. extra tags lives in `tags`.
68
56
  userId: v.string(),
69
57
  actionName: v.optional(v.string()),
58
+ tags: v.optional(v.array(vTag)),
70
59
  model: v.string(),
71
60
  // pessimistic holds placed at start; reconciled to actual on settle
72
61
  estimatedNanos: v.optional(v.number()),
@@ -122,12 +111,12 @@ export default defineSchema({
122
111
  )
123
112
  ),
124
113
  models: v.optional(v.array(v.string())),
125
- // Deployment-wide ("global") spend cap across ALL users and actions. The
126
- // running totals live in a sharded counter (high write throughput); only
127
- // the limit config lives here. The cap is enforced approximately — the
128
- // sharded total is read without a reservation, so under heavy concurrency
129
- // it can overshoot by a bounded amount. Right for a global killswitch;
130
- // per-user/per-action caps stay exact via reserve/settle.
114
+ // Deployment-wide ("global") spend cap across ALL requests. Running totals
115
+ // live in a sharded counter (high write throughput) since every request
116
+ // touches it; only the limit config lives here. Enforced approximately —
117
+ // the sharded total is read without a reservation, so under heavy
118
+ // concurrency it can overshoot by a bounded amount. Right for a global
119
+ // killswitch; per-bucket caps stay exact via reserve/settle.
131
120
  globalDailySpendLimitNanos: v.optional(v.number()),
132
121
  globalLifetimeSpendLimitNanos: v.optional(v.number()),
133
122
  globalEnforcement: v.optional(