@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.
- 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 +5 -5
- 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
|
@@ -32,18 +32,24 @@ async function settle(t: any, requestId: any, p = 10, c = 5) {
|
|
|
32
32
|
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
|
33
33
|
vi.useRealTimers();
|
|
34
34
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
35
|
+
const setUserLimits = (t: any, userId: string, limits: any) =>
|
|
36
|
+
t.mutation(api.lib.setBucketLimits, {
|
|
37
|
+
dimension: "user",
|
|
38
|
+
value: userId,
|
|
39
|
+
...limits,
|
|
40
|
+
});
|
|
41
|
+
const bucketOf = async (t: any, dimension: string, value: string) =>
|
|
42
|
+
(await t.query(api.lib.listBuckets, { dimension })).find(
|
|
43
|
+
(b: any) => b.value === value
|
|
44
|
+
);
|
|
45
|
+
const userOf = (t: any, userId: string) => bucketOf(t, "user", userId);
|
|
37
46
|
|
|
38
47
|
describe("reserve / settle spend caps", () => {
|
|
39
48
|
test("a daily cap below one request's reservation blocks up front", async () => {
|
|
40
49
|
const t = convexTest(schema, modules);
|
|
41
50
|
// one gpt-4o-mini request reserves ~480_000 nanodollars ($0.00048); a
|
|
42
51
|
// 1_000-nano ($0.000001) cap can't fit it.
|
|
43
|
-
await t
|
|
44
|
-
userId: "u",
|
|
45
|
-
dailySpendLimitNanos: 1_000,
|
|
46
|
-
});
|
|
52
|
+
await setUserLimits(t, "u", { dailySpendLimitNanos: 1_000 });
|
|
47
53
|
const r = await start(t, { userId: "u" });
|
|
48
54
|
expect(r.allowed).toBe(false);
|
|
49
55
|
expect(r.code).toBe("user_daily_spend_limit");
|
|
@@ -51,10 +57,7 @@ describe("reserve / settle spend caps", () => {
|
|
|
51
57
|
|
|
52
58
|
test("reservation is released and settled to the real cost", async () => {
|
|
53
59
|
const t = convexTest(schema, modules);
|
|
54
|
-
await t
|
|
55
|
-
userId: "u",
|
|
56
|
-
dailySpendLimitNanos: 1_000_000_000, // $1/day
|
|
57
|
-
});
|
|
60
|
+
await setUserLimits(t, "u", { dailySpendLimitNanos: 1_000_000_000 }); // $1/day
|
|
58
61
|
const r = await start(t, { userId: "u" });
|
|
59
62
|
expect(r.allowed).toBe(true);
|
|
60
63
|
await settle(t, r.requestId, 1_000_000, 1_000_000); // 1M in, 1M out
|
|
@@ -67,6 +70,38 @@ describe("reserve / settle spend caps", () => {
|
|
|
67
70
|
});
|
|
68
71
|
});
|
|
69
72
|
|
|
73
|
+
describe("tagged attribution buckets", () => {
|
|
74
|
+
test("a cap on a custom tag blocks, and settlement accrues to every bucket", async () => {
|
|
75
|
+
const t = convexTest(schema, modules);
|
|
76
|
+
// A tiny cap on customer "acme" — the user is uncapped.
|
|
77
|
+
await t.mutation(api.lib.setBucketLimits, {
|
|
78
|
+
dimension: "customer",
|
|
79
|
+
value: "acme",
|
|
80
|
+
dailySpendLimitNanos: 1_000,
|
|
81
|
+
});
|
|
82
|
+
const blocked = await start(t, {
|
|
83
|
+
userId: "u",
|
|
84
|
+
tags: [{ dimension: "customer", value: "acme" }],
|
|
85
|
+
});
|
|
86
|
+
expect(blocked.allowed).toBe(false);
|
|
87
|
+
expect(blocked.code).toBe("customer_daily_spend_limit");
|
|
88
|
+
|
|
89
|
+
// A different customer with no cap goes through, and the spend lands on
|
|
90
|
+
// BOTH the user bucket and the customer bucket.
|
|
91
|
+
const ok = await start(t, {
|
|
92
|
+
userId: "u",
|
|
93
|
+
tags: [{ dimension: "customer", value: "globex" }],
|
|
94
|
+
});
|
|
95
|
+
expect(ok.allowed).toBe(true);
|
|
96
|
+
await settle(t, ok.requestId, 1_000_000, 1_000_000); // $0.75
|
|
97
|
+
const user = await userOf(t, "u");
|
|
98
|
+
const cust = await bucketOf(t, "customer", "globex");
|
|
99
|
+
expect(user.totalSpendNanos).toBe(750_000_000);
|
|
100
|
+
expect(cust.totalSpendNanos).toBe(750_000_000);
|
|
101
|
+
expect(cust.totalRequests).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
70
105
|
describe("D-00 exactly-once settlement", () => {
|
|
71
106
|
test("a duplicate finishRequest does not double-count", async () => {
|
|
72
107
|
const t = convexTest(schema, modules);
|
|
@@ -85,7 +120,7 @@ describe("D-00 exactly-once settlement", () => {
|
|
|
85
120
|
describe("token quotas", () => {
|
|
86
121
|
test("a tiny daily token cap blocks (estimate exceeds it)", async () => {
|
|
87
122
|
const t = convexTest(schema, modules);
|
|
88
|
-
await t
|
|
123
|
+
await setUserLimits(t, "u", { dailyTokenLimit: 10 });
|
|
89
124
|
const r = await start(t, { userId: "u" });
|
|
90
125
|
expect(r.allowed).toBe(false);
|
|
91
126
|
expect(r.code).toBe("user_daily_token_limit");
|
|
@@ -95,15 +130,14 @@ describe("token quotas", () => {
|
|
|
95
130
|
describe("soft enforcement", () => {
|
|
96
131
|
test("over a soft budget: allowed, warned, flagged overBudget", async () => {
|
|
97
132
|
const t = convexTest(schema, modules);
|
|
98
|
-
await t
|
|
99
|
-
userId: "u",
|
|
133
|
+
await setUserLimits(t, "u", {
|
|
100
134
|
dailySpendLimitNanos: 1, // 1 nanodollar — one estimate blows past it
|
|
101
135
|
enforcement: "soft",
|
|
102
136
|
});
|
|
103
137
|
const r = await start(t, { userId: "u" });
|
|
104
138
|
expect(r.allowed).toBe(true);
|
|
105
139
|
expect(r.warnings.length).toBeGreaterThan(0);
|
|
106
|
-
const req = await t.query(api.lib.getRequest, { requestId: r.requestId })
|
|
140
|
+
const req = (await t.query(api.lib.getRequest, { requestId: r.requestId }))!;
|
|
107
141
|
expect(req.overBudget).toBe(true);
|
|
108
142
|
});
|
|
109
143
|
});
|
|
@@ -145,7 +179,7 @@ describe("F-04 fail-closed pricing", () => {
|
|
|
145
179
|
const u = await userOf(t, "u");
|
|
146
180
|
// conservative = max over table = {$3 in, $15 out}/Mtok => $18 = 18e9 nano.
|
|
147
181
|
expect(u.totalSpendNanos).toBe(18_000_000_000);
|
|
148
|
-
const req = await t.query(api.lib.getRequest, { requestId: r.requestId })
|
|
182
|
+
const req = (await t.query(api.lib.getRequest, { requestId: r.requestId }))!;
|
|
149
183
|
expect(req.unpricedModel).toBe(true);
|
|
150
184
|
});
|
|
151
185
|
});
|