@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.
@@ -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 userOf = async (t: any, userId: string) =>
36
- (await t.query(api.lib.listUsers, {})).find((u: any) => u.userId === userId);
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.mutation(api.lib.setLimits, {
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.mutation(api.lib.setLimits, {
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.mutation(api.lib.setLimits, { userId: "u", dailyTokenLimit: 10 });
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.mutation(api.lib.setLimits, {
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
  });