@convex-dev/ai-budget 0.0.2-alpha.14 → 0.0.2-alpha.15
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 +44 -8
- package/dist/client/index.d.ts +33 -0
- package/dist/client/index.js +1 -1
- package/dist/component/_generated/api.d.ts +1 -0
- package/dist/component/convex.config.js +2 -0
- package/dist/component/lib.d.ts +32 -0
- package/dist/component/lib.js +166 -122
- package/dist/component/schema.d.ts +53 -1
- package/dist/component/schema.js +31 -1
- package/package.json +3 -2
- package/src/client/index.ts +2 -1
- package/src/client/webhook.test.ts +32 -0
- package/src/component/_generated/api.ts +1 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.test.ts +246 -29
- package/src/component/lib.ts +177 -135
- package/src/component/schema.ts +31 -1
package/README.md
CHANGED
|
@@ -282,6 +282,8 @@ ai.registerWebhook(http, {
|
|
|
282
282
|
|
|
283
283
|
`begin` returns the admission result (it doesn't throw — check `allowed`).
|
|
284
284
|
`settle` is idempotent (exactly-once server-side), so a retried webhook is safe.
|
|
285
|
+
The webhook resolver receives the original, unread `Request`, allowing signature
|
|
286
|
+
verification over the exact raw body; the parsed JSON comes from a clone.
|
|
285
287
|
|
|
286
288
|
### Replay
|
|
287
289
|
|
|
@@ -315,6 +317,25 @@ different dimensions. Each exposes:
|
|
|
315
317
|
The identifier field is `userId` for `ai.users`, `name` for `ai.actions`, and
|
|
316
318
|
`value` for `ai.tag(d)`. For example: `ai.tag("customer").setLimits(ctx, { value: "acme", … })`.
|
|
317
319
|
|
|
320
|
+
### Request rate limits
|
|
321
|
+
|
|
322
|
+
`requestsPerMinute` uses `@convex-dev/rate-limiter` 0.4.0 with a transactional
|
|
323
|
+
**token bucket** for each user, action, or custom tag. A limit of 60 allows an
|
|
324
|
+
initial burst of 60 requests and then refills at one request per second, up to
|
|
325
|
+
60. Zero blocks all requests. Only admitted requests consume capacity: a budget,
|
|
326
|
+
concurrency, or another bucket's rate rejection consumes none.
|
|
327
|
+
|
|
328
|
+
This replaces the previous rolling 60-second request-log count. On upgrading,
|
|
329
|
+
rate balances start full; historical requests are not imported. Budget balances,
|
|
330
|
+
reservations, and spend history are preserved. Deploy the component update to
|
|
331
|
+
mount its nested Rate Limiter component; applications do not register it separately.
|
|
332
|
+
Changing a rate keeps its existing balance (clamped to the new capacity when
|
|
333
|
+
checked); deleting and recreating a bucket starts a fresh balance.
|
|
334
|
+
|
|
335
|
+
Rate limits remain transactional. Rate Limiter's asynchronous mode is not enabled
|
|
336
|
+
here because simultaneous admissions must respect every configured bucket.
|
|
337
|
+
Global spend accounting continues to use the existing sharded counter.
|
|
338
|
+
|
|
318
339
|
### Setting caps
|
|
319
340
|
|
|
320
341
|
```ts
|
|
@@ -549,21 +570,26 @@ this design). `ai-budget` instead **reserves then settles**:
|
|
|
549
570
|
schedules a fold of the real cost into the totals, releasing the reservation —
|
|
550
571
|
so a request is never orphaned mid-flight.
|
|
551
572
|
3. **Reconcile.** A once-a-minute cron folds any stragglers and releases
|
|
552
|
-
reservations
|
|
553
|
-
|
|
554
|
-
|
|
573
|
+
expired reservations using indexed deadlines. Expiry releases the hold but
|
|
574
|
+
leaves billing open: a late completion records its final charge once without
|
|
575
|
+
releasing the hold again. After content retention, unresolved requests retain
|
|
576
|
+
a small billing record with prompts and responses removed.
|
|
555
577
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
578
|
+
Requests record the exact bucket IDs and calendar windows they reserve.
|
|
579
|
+
Settlement cannot release another request's hold, including across midnight or
|
|
580
|
+
month boundaries. Admission reads separate policy documents; only capped buckets
|
|
581
|
+
require reads of accounting state. Reporting updates still share bucket totals,
|
|
582
|
+
but do not write the policy documents used by uncapped admission.
|
|
559
583
|
|
|
560
584
|
**One admission rule, all scopes.** Every per-bucket cap — user, action, or any
|
|
561
585
|
tag — runs through the *same* admission check: a request is admitted only when
|
|
562
586
|
`committed + reserved + estimate ≤ cap` (bumps included) for **every** bucket it
|
|
563
587
|
touches. Each per-bucket cap reserves on a single document, making concurrent
|
|
564
588
|
admission atomic. The **global** killswitch is backed by a sharded counter for
|
|
565
|
-
throughput
|
|
566
|
-
reservation, so
|
|
589
|
+
throughput. Its sum is transactional but excludes unfinished and not-yet-folded
|
|
590
|
+
requests and has no cross-request reservation, so global admissions can overshoot.
|
|
591
|
+
Global usage is recorded even when limits are disabled. Historical usage omitted
|
|
592
|
+
by older versions is not automatically reconstructed by this upgrade.
|
|
567
593
|
|
|
568
594
|
Reservations are estimates, not provider-side maximum charges. If a response uses
|
|
569
595
|
more tokens or costs more than estimated, settlement records the real amount and
|
|
@@ -651,6 +677,16 @@ handles rather than taking a dependency on either sibling component.
|
|
|
651
677
|
|
|
652
678
|
---
|
|
653
679
|
|
|
680
|
+
|
|
681
|
+
## Accounting upgrade notes
|
|
682
|
+
|
|
683
|
+
New request fields are optional for existing deployments. Policy documents are
|
|
684
|
+
created on first admission or limit update; pending requests without deadlines
|
|
685
|
+
are migrated in batches by reconciliation. Existing reservations without ownership
|
|
686
|
+
metadata use their creation period and current capped buckets as a compatibility
|
|
687
|
+
fallback. Exact historical hold ownership cannot be reconstructed if those caps
|
|
688
|
+
changed before the upgrade. New requests always store explicit ownership.
|
|
689
|
+
|
|
654
690
|
## Development
|
|
655
691
|
|
|
656
692
|
```sh
|
package/dist/client/index.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export type ChatResult = {
|
|
|
84
84
|
};
|
|
85
85
|
/** Limits/controls settable on any budget bucket (user, action, or tag). */
|
|
86
86
|
export type BucketLimits = {
|
|
87
|
+
/** Token-bucket refill per minute and burst capacity; 0 blocks all requests. */
|
|
87
88
|
requestsPerMinute?: number;
|
|
88
89
|
maxConcurrent?: number;
|
|
89
90
|
dailySpendLimitNanos?: number;
|
|
@@ -237,6 +238,14 @@ export declare class AIBudget {
|
|
|
237
238
|
dimension: string;
|
|
238
239
|
value: string;
|
|
239
240
|
}[] | undefined;
|
|
241
|
+
heldBucketIds?: string[] | undefined;
|
|
242
|
+
reservationDay?: string | undefined;
|
|
243
|
+
reservationMonth?: string | undefined;
|
|
244
|
+
reservationReleased?: boolean | undefined;
|
|
245
|
+
reservationExpired?: boolean | undefined;
|
|
246
|
+
contentPurged?: boolean | undefined;
|
|
247
|
+
expiresAt?: number | undefined;
|
|
248
|
+
finishedAt?: number | undefined;
|
|
240
249
|
estimatedNanos?: number | undefined;
|
|
241
250
|
estimatedTokens?: number | undefined;
|
|
242
251
|
unpricedModel?: boolean | undefined;
|
|
@@ -273,6 +282,14 @@ export declare class AIBudget {
|
|
|
273
282
|
dimension: string;
|
|
274
283
|
value: string;
|
|
275
284
|
}[] | undefined;
|
|
285
|
+
heldBucketIds?: string[] | undefined;
|
|
286
|
+
reservationDay?: string | undefined;
|
|
287
|
+
reservationMonth?: string | undefined;
|
|
288
|
+
reservationReleased?: boolean | undefined;
|
|
289
|
+
reservationExpired?: boolean | undefined;
|
|
290
|
+
contentPurged?: boolean | undefined;
|
|
291
|
+
expiresAt?: number | undefined;
|
|
292
|
+
finishedAt?: number | undefined;
|
|
276
293
|
estimatedNanos?: number | undefined;
|
|
277
294
|
estimatedTokens?: number | undefined;
|
|
278
295
|
unpricedModel?: boolean | undefined;
|
|
@@ -310,6 +327,14 @@ export declare class AIBudget {
|
|
|
310
327
|
dimension: string;
|
|
311
328
|
value: string;
|
|
312
329
|
}[] | undefined;
|
|
330
|
+
heldBucketIds?: string[] | undefined;
|
|
331
|
+
reservationDay?: string | undefined;
|
|
332
|
+
reservationMonth?: string | undefined;
|
|
333
|
+
reservationReleased?: boolean | undefined;
|
|
334
|
+
reservationExpired?: boolean | undefined;
|
|
335
|
+
contentPurged?: boolean | undefined;
|
|
336
|
+
expiresAt?: number | undefined;
|
|
337
|
+
finishedAt?: number | undefined;
|
|
313
338
|
estimatedNanos?: number | undefined;
|
|
314
339
|
estimatedTokens?: number | undefined;
|
|
315
340
|
unpricedModel?: boolean | undefined;
|
|
@@ -343,6 +368,14 @@ export declare class AIBudget {
|
|
|
343
368
|
dimension: string;
|
|
344
369
|
value: string;
|
|
345
370
|
}[] | undefined;
|
|
371
|
+
heldBucketIds?: string[] | undefined;
|
|
372
|
+
reservationDay?: string | undefined;
|
|
373
|
+
reservationMonth?: string | undefined;
|
|
374
|
+
reservationReleased?: boolean | undefined;
|
|
375
|
+
reservationExpired?: boolean | undefined;
|
|
376
|
+
contentPurged?: boolean | undefined;
|
|
377
|
+
expiresAt?: number | undefined;
|
|
378
|
+
finishedAt?: number | undefined;
|
|
346
379
|
estimatedNanos?: number | undefined;
|
|
347
380
|
estimatedTokens?: number | undefined;
|
|
348
381
|
unpricedModel?: boolean | undefined;
|
package/dist/client/index.js
CHANGED
|
@@ -692,7 +692,7 @@ export class AIBudget {
|
|
|
692
692
|
path,
|
|
693
693
|
method: "POST",
|
|
694
694
|
handler: httpActionGeneric(async (ctx, request) => {
|
|
695
|
-
const body = await request.json().catch(() => ({}));
|
|
695
|
+
const body = await request.clone().json().catch(() => ({}));
|
|
696
696
|
const settle = await opts.resolve(ctx, request, body);
|
|
697
697
|
if (!settle)
|
|
698
698
|
return new Response("ignored", { status: 202 });
|
|
@@ -33,5 +33,6 @@ export declare const api: FilterApi<typeof fullApi, FunctionReference<any, "publ
|
|
|
33
33
|
export declare const internal: FilterApi<typeof fullApi, FunctionReference<any, "internal">>;
|
|
34
34
|
export declare const components: {
|
|
35
35
|
shardedCounter: import("@convex-dev/sharded-counter/_generated/component.js").ComponentApi<"shardedCounter">;
|
|
36
|
+
rateLimiter: import("@convex-dev/rate-limiter/_generated/component.js").ComponentApi<"rateLimiter">;
|
|
36
37
|
};
|
|
37
38
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineComponent } from "convex/server";
|
|
2
2
|
import shardedCounter from "@convex-dev/sharded-counter/convex.config";
|
|
3
|
+
import rateLimiter from "@convex-dev/rate-limiter/convex.config";
|
|
3
4
|
const component = defineComponent("aiBudget");
|
|
4
5
|
// Global spend totals use a sharded counter for high write throughput.
|
|
5
6
|
component.use(shardedCounter);
|
|
7
|
+
component.use(rateLimiter);
|
|
6
8
|
export default component;
|
package/dist/component/lib.d.ts
CHANGED
|
@@ -58,6 +58,14 @@ export declare const lineage: import("convex/server").RegisteredQuery<"public",
|
|
|
58
58
|
dimension: string;
|
|
59
59
|
value: string;
|
|
60
60
|
}[] | undefined;
|
|
61
|
+
heldBucketIds?: import("convex/values").GenericId<"buckets">[] | undefined;
|
|
62
|
+
reservationDay?: string | undefined;
|
|
63
|
+
reservationMonth?: string | undefined;
|
|
64
|
+
reservationReleased?: boolean | undefined;
|
|
65
|
+
reservationExpired?: boolean | undefined;
|
|
66
|
+
contentPurged?: boolean | undefined;
|
|
67
|
+
expiresAt?: number | undefined;
|
|
68
|
+
finishedAt?: number | undefined;
|
|
61
69
|
estimatedNanos?: number | undefined;
|
|
62
70
|
estimatedTokens?: number | undefined;
|
|
63
71
|
unpricedModel?: boolean | undefined;
|
|
@@ -89,6 +97,14 @@ export declare const lineage: import("convex/server").RegisteredQuery<"public",
|
|
|
89
97
|
dimension: string;
|
|
90
98
|
value: string;
|
|
91
99
|
}[] | undefined;
|
|
100
|
+
heldBucketIds?: import("convex/values").GenericId<"buckets">[] | undefined;
|
|
101
|
+
reservationDay?: string | undefined;
|
|
102
|
+
reservationMonth?: string | undefined;
|
|
103
|
+
reservationReleased?: boolean | undefined;
|
|
104
|
+
reservationExpired?: boolean | undefined;
|
|
105
|
+
contentPurged?: boolean | undefined;
|
|
106
|
+
expiresAt?: number | undefined;
|
|
107
|
+
finishedAt?: number | undefined;
|
|
92
108
|
estimatedNanos?: number | undefined;
|
|
93
109
|
estimatedTokens?: number | undefined;
|
|
94
110
|
unpricedModel?: boolean | undefined;
|
|
@@ -123,6 +139,14 @@ export declare const getRequest: import("convex/server").RegisteredQuery<"public
|
|
|
123
139
|
dimension: string;
|
|
124
140
|
value: string;
|
|
125
141
|
}[] | undefined;
|
|
142
|
+
heldBucketIds?: import("convex/values").GenericId<"buckets">[] | undefined;
|
|
143
|
+
reservationDay?: string | undefined;
|
|
144
|
+
reservationMonth?: string | undefined;
|
|
145
|
+
reservationReleased?: boolean | undefined;
|
|
146
|
+
reservationExpired?: boolean | undefined;
|
|
147
|
+
contentPurged?: boolean | undefined;
|
|
148
|
+
expiresAt?: number | undefined;
|
|
149
|
+
finishedAt?: number | undefined;
|
|
126
150
|
estimatedNanos?: number | undefined;
|
|
127
151
|
estimatedTokens?: number | undefined;
|
|
128
152
|
unpricedModel?: boolean | undefined;
|
|
@@ -159,6 +183,14 @@ export declare const listRequests: import("convex/server").RegisteredQuery<"publ
|
|
|
159
183
|
dimension: string;
|
|
160
184
|
value: string;
|
|
161
185
|
}[] | undefined;
|
|
186
|
+
heldBucketIds?: import("convex/values").GenericId<"buckets">[] | undefined;
|
|
187
|
+
reservationDay?: string | undefined;
|
|
188
|
+
reservationMonth?: string | undefined;
|
|
189
|
+
reservationReleased?: boolean | undefined;
|
|
190
|
+
reservationExpired?: boolean | undefined;
|
|
191
|
+
contentPurged?: boolean | undefined;
|
|
192
|
+
expiresAt?: number | undefined;
|
|
193
|
+
finishedAt?: number | undefined;
|
|
162
194
|
estimatedNanos?: number | undefined;
|
|
163
195
|
estimatedTokens?: number | undefined;
|
|
164
196
|
unpricedModel?: boolean | undefined;
|