@convex-dev/ai-budget 0.0.2-alpha.3 → 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.
- package/README.md +49 -11
- package/dist/client/index.d.ts +130 -6
- package/dist/client/index.js +71 -10
- package/dist/component/_generated/component.d.ts +22 -24
- package/dist/component/lib.d.ts +57 -40
- package/dist/component/lib.js +264 -276
- package/dist/component/schema.d.ts +30 -53
- package/dist/component/schema.js +27 -38
- package/package.json +1 -1
- package/src/client/index.ts +114 -12
- package/src/component/_generated/component.ts +29 -32
- package/src/component/lib.test.ts +49 -15
- package/src/component/lib.ts +285 -285
- package/src/component/schema.ts +28 -39
package/src/component/schema.ts
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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")
|
|
22
|
-
//
|
|
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
|
-
})
|
|
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
|
|
126
|
-
//
|
|
127
|
-
// the limit config lives here.
|
|
128
|
-
// sharded total is read without a reservation, so under heavy
|
|
129
|
-
// it can overshoot by a bounded amount. Right for a global
|
|
130
|
-
// per-
|
|
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(
|