@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/README.md
CHANGED
|
@@ -25,10 +25,11 @@ full audit log you can replay later.
|
|
|
25
25
|
|---|---|
|
|
26
26
|
| **Usage & cost tracking** | Every request stored with messages, response, tokens, latency, and per-request cost. |
|
|
27
27
|
| **Attribution** | Each call is attributed to a `userId` **and** to the Convex action that made it — auto-detected via `ctx.meta`, no manual tagging. Running totals per user and per action. |
|
|
28
|
+
| **Tagged budgets** | `user` and `action` are just built-in *dimensions* — add your own (team, project, customer, env, feature…) by passing `tags`, and cap any of them with `ai.tag("customer").setLimits(...)`. One request can be billed to several buckets at once. |
|
|
28
29
|
| **Spend & token limits** | Per-user daily / lifetime **spend** and **token** budgets, plus a requests-per-minute rate limit and a block switch. |
|
|
29
30
|
| **Concurrency-safe caps** | A reserve-then-settle design makes admission a true atomic check — concurrent in-flight requests can't blow past the cap (a naive implementation overshoots ~40×). |
|
|
30
31
|
| **Hard or soft** | Each limit either **blocks** (`hard`) or **allows-with-a-warning** (`soft`). |
|
|
31
|
-
| **Per-feature budgets** | Cap or
|
|
32
|
+
| **Per-feature budgets** | Cap or block a whole action (e.g. `summarize`) independently of any user. |
|
|
32
33
|
| **Global killswitch** | A deployment-wide spend cap across all users and actions (sharded for throughput; enforced approximately). |
|
|
33
34
|
| **One-time bumps** | "Approve another $X" at any level (user / action / global) without changing the standing cap — daily bumps are today-only, lifetime bumps permanent. |
|
|
34
35
|
| **Model policy** | Allow/deny lists for models; an unknown/unpriced model **fails closed** (charged a conservative max, never $0). |
|
|
@@ -136,6 +137,7 @@ ai.chat(ctx, {
|
|
|
136
137
|
messages?, // [{ role, content }]
|
|
137
138
|
model?, // defaults to defaultModel
|
|
138
139
|
action?, // attribution name; defaults to the calling Convex action
|
|
140
|
+
tags?, // extra dimensions: [{ dimension: "customer", value: "acme" }, …]
|
|
139
141
|
}): Promise<{ text, requestId, costNanos, promptTokens, completionTokens, warnings }>
|
|
140
142
|
```
|
|
141
143
|
|
|
@@ -208,10 +210,42 @@ ai.actions.setLimits(ctx, {
|
|
|
208
210
|
dailyTokenLimit?,
|
|
209
211
|
lifetimeTokenLimit?,
|
|
210
212
|
enforcement?, // "hard" | "soft"
|
|
211
|
-
|
|
213
|
+
blocked?, // kill switch for the whole feature
|
|
212
214
|
})
|
|
213
215
|
```
|
|
214
216
|
|
|
217
|
+
### Tagged budgets (custom dimensions)
|
|
218
|
+
|
|
219
|
+
`user` and `action` are the two built-in *dimensions*. To classify or budget
|
|
220
|
+
along any other axis — team, project, tenant, customer, environment, feature —
|
|
221
|
+
attach `tags` to a call and cap a value with `ai.tag(dimension)`:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// Bill this call to a user, an action (implicit), AND a customer + env.
|
|
225
|
+
await ai.chat(ctx, {
|
|
226
|
+
prompt,
|
|
227
|
+
tags: [
|
|
228
|
+
{ dimension: "customer", value: "acme" },
|
|
229
|
+
{ dimension: "env", value: "prod" },
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Cap the customer "acme" to $50/day — independent of any per-user cap.
|
|
234
|
+
await ai.tag("customer").setLimits(ctx, {
|
|
235
|
+
value: "acme",
|
|
236
|
+
dailySpendLimitNanos: 50 * 1_000_000_000,
|
|
237
|
+
});
|
|
238
|
+
await ai.tag("customer").bump(ctx, { value: "acme", dailyNanos: 10 * 1_000_000_000 });
|
|
239
|
+
await ai.tag("customer").list(ctx); // every customer's spend & caps
|
|
240
|
+
await ai.tag("customer").get(ctx, { value: "acme" }); // one bucket
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
A single request is admitted only if it fits **every** bucket it touches (user,
|
|
244
|
+
action, and each tag) — the same exact reserve-then-settle check runs per bucket.
|
|
245
|
+
Uncapped buckets never serialize, so adding tags you don't cap is free at
|
|
246
|
+
admission; their running totals still accrue for reporting. `ai.users.*` and
|
|
247
|
+
`ai.actions.*` are simply sugar over `ai.tag("user")` / `ai.tag("action")`.
|
|
248
|
+
|
|
215
249
|
### Global (deployment-wide) budget
|
|
216
250
|
|
|
217
251
|
```ts
|
|
@@ -279,6 +313,7 @@ flagged `unpricedModel: true` so you know to add a real price.
|
|
|
279
313
|
```ts
|
|
280
314
|
ai.users.list(ctx) // per-user spend today / total / tokens / limits
|
|
281
315
|
ai.actions.list(ctx) // per-action spend & totals
|
|
316
|
+
ai.tag("customer").list(ctx) // spend & caps for any custom dimension
|
|
282
317
|
ai.requests.list(ctx, { userId?, limit? }) // the audit log (blocked attempts included)
|
|
283
318
|
```
|
|
284
319
|
|
|
@@ -305,15 +340,18 @@ same pre-spend total and all pass, so spend blows past the cap (measured at
|
|
|
305
340
|
**exactly-once** (a terminal request is never re-folded), so a slow request
|
|
306
341
|
that the reconciler already swept can't double-count when it finally returns.
|
|
307
342
|
|
|
308
|
-
Reservations are only taken on
|
|
309
|
-
traffic never serializes
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
343
|
+
Reservations are only taken on buckets that actually have a cap, so uncapped
|
|
344
|
+
traffic never serializes — this is what makes arbitrary `tags` cheap: a request
|
|
345
|
+
reserves on one row per *capped* dimension it carries, and nothing else. The
|
|
346
|
+
`error.md` file documents the adversarial audits this design survived, with live
|
|
347
|
+
repros.
|
|
348
|
+
|
|
349
|
+
**One guarantee, all scopes.** Every per-bucket cap — user, action, or any
|
|
350
|
+
custom tag dimension — and the global cap run through the *same* admission check:
|
|
351
|
+
a request is admitted only when `committed + reserved + estimate ≤ cap` (bumps
|
|
352
|
+
included) for **every** bucket it touches. The only thing that differs is the
|
|
353
|
+
holder: each per-bucket cap reserves on a single document, an exact atomic
|
|
354
|
+
check-and-reserve; the **global** killswitch is backed by a sharded
|
|
317
355
|
counter for throughput, so its committed total is read as an eventually-consistent
|
|
318
356
|
sum with no cross-request reservation. That makes the global cap **approximate** —
|
|
319
357
|
it can overshoot by a bounded amount under a burst — the deliberate
|
package/dist/client/index.d.ts
CHANGED
|
@@ -11,10 +11,21 @@ type UseApi<API> = Expand<{
|
|
|
11
11
|
export type AIBudgetApi = UseApi<typeof api>;
|
|
12
12
|
/** @deprecated use AIBudgetApi */
|
|
13
13
|
export type AIGatewayApi = AIBudgetApi;
|
|
14
|
+
/**
|
|
15
|
+
* One attribution tag: a (dimension, value) pair, e.g. {dimension:"customer",
|
|
16
|
+
* value:"acme"}. `user` and `action` are built-in dimensions (set via
|
|
17
|
+
* userId/action); use tags for anything else — team, project, tenant, env, ….
|
|
18
|
+
* Any tagged bucket can carry its own budget (see `ai.tag(dimension)`).
|
|
19
|
+
*/
|
|
20
|
+
export type Tag = {
|
|
21
|
+
dimension: string;
|
|
22
|
+
value: string;
|
|
23
|
+
};
|
|
14
24
|
/** Fired when a request is admitted over a *soft* limit. */
|
|
15
25
|
export type SoftLimitInfo = {
|
|
16
26
|
userId: string;
|
|
17
27
|
action?: string;
|
|
28
|
+
tags?: Tag[];
|
|
18
29
|
requestId: string;
|
|
19
30
|
warnings: string[];
|
|
20
31
|
};
|
|
@@ -76,6 +87,8 @@ export declare class AIBudget {
|
|
|
76
87
|
rerunOf?: string;
|
|
77
88
|
/** Attribute spend to this action name. Defaults to the calling Convex action. */
|
|
78
89
|
action?: string;
|
|
90
|
+
/** Extra attribution dimensions to bill/limit (team, customer, env, …). */
|
|
91
|
+
tags?: Tag[];
|
|
79
92
|
}): Promise<ChatResult>;
|
|
80
93
|
/**
|
|
81
94
|
* An AI SDK LanguageModel that enforces limits and records usage/cost for
|
|
@@ -87,6 +100,8 @@ export declare class AIBudget {
|
|
|
87
100
|
userId?: string;
|
|
88
101
|
model?: string;
|
|
89
102
|
action?: string;
|
|
103
|
+
/** Extra attribution dimensions to bill/limit (team, customer, env, …). */
|
|
104
|
+
tags?: Tag[];
|
|
90
105
|
}): LanguageModel;
|
|
91
106
|
private rerunImpl;
|
|
92
107
|
/** The request audit log, replay, and re-run lineage. */
|
|
@@ -98,6 +113,10 @@ export declare class AIBudget {
|
|
|
98
113
|
_id: string;
|
|
99
114
|
_creationTime: number;
|
|
100
115
|
actionName?: string | undefined;
|
|
116
|
+
tags?: {
|
|
117
|
+
dimension: string;
|
|
118
|
+
value: string;
|
|
119
|
+
}[] | undefined;
|
|
101
120
|
estimatedNanos?: number | undefined;
|
|
102
121
|
estimatedTokens?: number | undefined;
|
|
103
122
|
unpricedModel?: boolean | undefined;
|
|
@@ -127,6 +146,10 @@ export declare class AIBudget {
|
|
|
127
146
|
_id: string;
|
|
128
147
|
_creationTime: number;
|
|
129
148
|
actionName?: string | undefined;
|
|
149
|
+
tags?: {
|
|
150
|
+
dimension: string;
|
|
151
|
+
value: string;
|
|
152
|
+
}[] | undefined;
|
|
130
153
|
estimatedNanos?: number | undefined;
|
|
131
154
|
estimatedTokens?: number | undefined;
|
|
132
155
|
unpricedModel?: boolean | undefined;
|
|
@@ -152,6 +175,10 @@ export declare class AIBudget {
|
|
|
152
175
|
_id: string;
|
|
153
176
|
_creationTime: number;
|
|
154
177
|
actionName?: string | undefined;
|
|
178
|
+
tags?: {
|
|
179
|
+
dimension: string;
|
|
180
|
+
value: string;
|
|
181
|
+
}[] | undefined;
|
|
155
182
|
estimatedNanos?: number | undefined;
|
|
156
183
|
estimatedTokens?: number | undefined;
|
|
157
184
|
unpricedModel?: boolean | undefined;
|
|
@@ -181,7 +208,100 @@ export declare class AIBudget {
|
|
|
181
208
|
model?: string;
|
|
182
209
|
}) => Promise<ChatResult>;
|
|
183
210
|
};
|
|
184
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Budgets and controls for an arbitrary attribution dimension — the
|
|
213
|
+
* generalization of `users`/`actions`. Give it any dimension name (team,
|
|
214
|
+
* project, tenant, customer, env, feature, …) and set caps per value:
|
|
215
|
+
*
|
|
216
|
+
* ai.tag("customer").setLimits(ctx, { value: "acme", dailySpendLimitNanos });
|
|
217
|
+
* ai.tag("customer").list(ctx);
|
|
218
|
+
*
|
|
219
|
+
* Attribute a call to it by passing `tags` to `chat`/`languageModel`.
|
|
220
|
+
*/
|
|
221
|
+
tag(dimension: string): {
|
|
222
|
+
/** All buckets in this dimension. */
|
|
223
|
+
list: (ctx: RunQueryCtx) => Promise<{
|
|
224
|
+
spendTodayNanos: number;
|
|
225
|
+
_id: string;
|
|
226
|
+
_creationTime: number;
|
|
227
|
+
requestsPerMinute?: number | undefined;
|
|
228
|
+
dailySpendLimitNanos?: number | undefined;
|
|
229
|
+
lifetimeSpendLimitNanos?: number | undefined;
|
|
230
|
+
dailyTokenLimit?: number | undefined;
|
|
231
|
+
lifetimeTokenLimit?: number | undefined;
|
|
232
|
+
blocked?: boolean | undefined;
|
|
233
|
+
enforcement?: "hard" | "soft" | undefined;
|
|
234
|
+
dailyBumpNanos?: number | undefined;
|
|
235
|
+
lifetimeBumpNanos?: number | undefined;
|
|
236
|
+
bumpDayStamp?: string | undefined;
|
|
237
|
+
tokensToday?: number | undefined;
|
|
238
|
+
reservedTodayNanos?: number | undefined;
|
|
239
|
+
reservedTotalNanos?: number | undefined;
|
|
240
|
+
reservedTodayTokens?: number | undefined;
|
|
241
|
+
reservedTotalTokens?: number | undefined;
|
|
242
|
+
pendingCount?: number | undefined;
|
|
243
|
+
dimension: string;
|
|
244
|
+
value: string;
|
|
245
|
+
totalSpendNanos: number;
|
|
246
|
+
totalRequests: number;
|
|
247
|
+
totalTokens: number;
|
|
248
|
+
dayStamp: string;
|
|
249
|
+
}[]>;
|
|
250
|
+
/** One bucket's limits + spend (null if it has none yet). */
|
|
251
|
+
get: (ctx: RunQueryCtx, args: {
|
|
252
|
+
value: string;
|
|
253
|
+
}) => Promise<{
|
|
254
|
+
spendTodayNanos: number;
|
|
255
|
+
_id: string;
|
|
256
|
+
_creationTime: number;
|
|
257
|
+
requestsPerMinute?: number | undefined;
|
|
258
|
+
dailySpendLimitNanos?: number | undefined;
|
|
259
|
+
lifetimeSpendLimitNanos?: number | undefined;
|
|
260
|
+
dailyTokenLimit?: number | undefined;
|
|
261
|
+
lifetimeTokenLimit?: number | undefined;
|
|
262
|
+
blocked?: boolean | undefined;
|
|
263
|
+
enforcement?: "hard" | "soft" | undefined;
|
|
264
|
+
dailyBumpNanos?: number | undefined;
|
|
265
|
+
lifetimeBumpNanos?: number | undefined;
|
|
266
|
+
bumpDayStamp?: string | undefined;
|
|
267
|
+
tokensToday?: number | undefined;
|
|
268
|
+
reservedTodayNanos?: number | undefined;
|
|
269
|
+
reservedTotalNanos?: number | undefined;
|
|
270
|
+
reservedTodayTokens?: number | undefined;
|
|
271
|
+
reservedTotalTokens?: number | undefined;
|
|
272
|
+
pendingCount?: number | undefined;
|
|
273
|
+
dimension: string;
|
|
274
|
+
value: string;
|
|
275
|
+
totalSpendNanos: number;
|
|
276
|
+
totalRequests: number;
|
|
277
|
+
totalTokens: number;
|
|
278
|
+
dayStamp: string;
|
|
279
|
+
} | null>;
|
|
280
|
+
setLimits: (ctx: RunMutationCtx, args: {
|
|
281
|
+
value: string;
|
|
282
|
+
requestsPerMinute?: number;
|
|
283
|
+
dailySpendLimitNanos?: number;
|
|
284
|
+
lifetimeSpendLimitNanos?: number;
|
|
285
|
+
dailyTokenLimit?: number;
|
|
286
|
+
lifetimeTokenLimit?: number;
|
|
287
|
+
enforcement?: "hard" | "soft";
|
|
288
|
+
blocked?: boolean;
|
|
289
|
+
}) => Promise<null>;
|
|
290
|
+
/** One-time "approve another $X" bump (daily is today-only). */
|
|
291
|
+
bump: (ctx: RunMutationCtx, args: {
|
|
292
|
+
value: string;
|
|
293
|
+
dailyNanos?: number;
|
|
294
|
+
lifetimeNanos?: number;
|
|
295
|
+
}) => Promise<null>;
|
|
296
|
+
/** Delete the bucket (for "user", also its request rows). */
|
|
297
|
+
delete: (ctx: RunMutationCtx, args: {
|
|
298
|
+
value: string;
|
|
299
|
+
}) => Promise<{
|
|
300
|
+
deletedThisBatch: number;
|
|
301
|
+
done: boolean;
|
|
302
|
+
}>;
|
|
303
|
+
};
|
|
304
|
+
/** Per-user budgets and controls — sugar over the "user" dimension. */
|
|
185
305
|
get users(): {
|
|
186
306
|
list: (ctx: RunQueryCtx) => Promise<{
|
|
187
307
|
spendTodayNanos: number;
|
|
@@ -203,7 +323,8 @@ export declare class AIBudget {
|
|
|
203
323
|
reservedTodayTokens?: number | undefined;
|
|
204
324
|
reservedTotalTokens?: number | undefined;
|
|
205
325
|
pendingCount?: number | undefined;
|
|
206
|
-
|
|
326
|
+
dimension: string;
|
|
327
|
+
value: string;
|
|
207
328
|
totalSpendNanos: number;
|
|
208
329
|
totalRequests: number;
|
|
209
330
|
totalTokens: number;
|
|
@@ -233,16 +354,18 @@ export declare class AIBudget {
|
|
|
233
354
|
done: boolean;
|
|
234
355
|
}>;
|
|
235
356
|
};
|
|
236
|
-
/** Per-action (per-feature) budgets. */
|
|
357
|
+
/** Per-action (per-feature) budgets — sugar over the "action" dimension. */
|
|
237
358
|
get actions(): {
|
|
238
359
|
list: (ctx: RunQueryCtx) => Promise<{
|
|
239
360
|
spendTodayNanos: number;
|
|
240
361
|
_id: string;
|
|
241
362
|
_creationTime: number;
|
|
363
|
+
requestsPerMinute?: number | undefined;
|
|
242
364
|
dailySpendLimitNanos?: number | undefined;
|
|
243
365
|
lifetimeSpendLimitNanos?: number | undefined;
|
|
244
366
|
dailyTokenLimit?: number | undefined;
|
|
245
367
|
lifetimeTokenLimit?: number | undefined;
|
|
368
|
+
blocked?: boolean | undefined;
|
|
246
369
|
enforcement?: "hard" | "soft" | undefined;
|
|
247
370
|
dailyBumpNanos?: number | undefined;
|
|
248
371
|
lifetimeBumpNanos?: number | undefined;
|
|
@@ -253,12 +376,12 @@ export declare class AIBudget {
|
|
|
253
376
|
reservedTodayTokens?: number | undefined;
|
|
254
377
|
reservedTotalTokens?: number | undefined;
|
|
255
378
|
pendingCount?: number | undefined;
|
|
256
|
-
|
|
379
|
+
dimension: string;
|
|
380
|
+
value: string;
|
|
257
381
|
totalSpendNanos: number;
|
|
258
382
|
totalRequests: number;
|
|
259
383
|
totalTokens: number;
|
|
260
384
|
dayStamp: string;
|
|
261
|
-
name: string;
|
|
262
385
|
}[]>;
|
|
263
386
|
setLimits: (ctx: RunMutationCtx, args: {
|
|
264
387
|
name: string;
|
|
@@ -267,7 +390,8 @@ export declare class AIBudget {
|
|
|
267
390
|
dailyTokenLimit?: number;
|
|
268
391
|
lifetimeTokenLimit?: number;
|
|
269
392
|
enforcement?: "hard" | "soft";
|
|
270
|
-
disabled
|
|
393
|
+
/** Hard-block this action (was `disabled`). */
|
|
394
|
+
blocked?: boolean;
|
|
271
395
|
}) => Promise<null>;
|
|
272
396
|
bump: (ctx: RunMutationCtx, args: {
|
|
273
397
|
name: string;
|
package/dist/client/index.js
CHANGED
|
@@ -110,6 +110,7 @@ export class AIBudget {
|
|
|
110
110
|
const started = await ctx.runMutation(this.component.lib.startRequest, {
|
|
111
111
|
userId,
|
|
112
112
|
actionName,
|
|
113
|
+
tags: args.tags,
|
|
113
114
|
model,
|
|
114
115
|
messages,
|
|
115
116
|
rerunOf: args.rerunOf,
|
|
@@ -123,7 +124,13 @@ export class AIBudget {
|
|
|
123
124
|
}
|
|
124
125
|
const requestId = started.requestId;
|
|
125
126
|
const warnings = started.warnings;
|
|
126
|
-
await this.fireSoftLimit({
|
|
127
|
+
await this.fireSoftLimit({
|
|
128
|
+
userId,
|
|
129
|
+
action: actionName,
|
|
130
|
+
tags: args.tags,
|
|
131
|
+
requestId,
|
|
132
|
+
warnings,
|
|
133
|
+
});
|
|
127
134
|
const start = Date.now();
|
|
128
135
|
try {
|
|
129
136
|
// The full chain (incl. system) is stored on the request for audit/replay,
|
|
@@ -172,6 +179,7 @@ export class AIBudget {
|
|
|
172
179
|
const started = await ctx.runMutation(component.lib.startRequest, {
|
|
173
180
|
userId,
|
|
174
181
|
actionName,
|
|
182
|
+
tags: opts.tags,
|
|
175
183
|
model: modelId,
|
|
176
184
|
messages: simplifyPrompt(params.prompt),
|
|
177
185
|
});
|
|
@@ -185,6 +193,7 @@ export class AIBudget {
|
|
|
185
193
|
await fireSoftLimit({
|
|
186
194
|
userId,
|
|
187
195
|
action: actionName,
|
|
196
|
+
tags: opts.tags,
|
|
188
197
|
requestId: started.requestId,
|
|
189
198
|
warnings: started.warnings,
|
|
190
199
|
});
|
|
@@ -278,6 +287,7 @@ export class AIBudget {
|
|
|
278
287
|
messages: args.messages ?? original.messages,
|
|
279
288
|
rerunOf: args.requestId,
|
|
280
289
|
action: original.actionName,
|
|
290
|
+
tags: original.tags,
|
|
281
291
|
});
|
|
282
292
|
}
|
|
283
293
|
// ---------- namespaced admin API ----------
|
|
@@ -292,25 +302,76 @@ export class AIBudget {
|
|
|
292
302
|
rerun: (ctx, args) => this.rerunImpl(ctx, args),
|
|
293
303
|
};
|
|
294
304
|
}
|
|
295
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* Budgets and controls for an arbitrary attribution dimension — the
|
|
307
|
+
* generalization of `users`/`actions`. Give it any dimension name (team,
|
|
308
|
+
* project, tenant, customer, env, feature, …) and set caps per value:
|
|
309
|
+
*
|
|
310
|
+
* ai.tag("customer").setLimits(ctx, { value: "acme", dailySpendLimitNanos });
|
|
311
|
+
* ai.tag("customer").list(ctx);
|
|
312
|
+
*
|
|
313
|
+
* Attribute a call to it by passing `tags` to `chat`/`languageModel`.
|
|
314
|
+
*/
|
|
315
|
+
tag(dimension) {
|
|
316
|
+
const c = this.component;
|
|
317
|
+
return {
|
|
318
|
+
/** All buckets in this dimension. */
|
|
319
|
+
list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension }),
|
|
320
|
+
/** One bucket's limits + spend (null if it has none yet). */
|
|
321
|
+
get: (ctx, args) => ctx.runQuery(c.lib.getBucket, { dimension, value: args.value }),
|
|
322
|
+
setLimits: (ctx, args) => ctx.runMutation(c.lib.setBucketLimits, { dimension, ...args }),
|
|
323
|
+
/** One-time "approve another $X" bump (daily is today-only). */
|
|
324
|
+
bump: (ctx, args) => ctx.runMutation(c.lib.bumpBucket, { dimension, ...args }),
|
|
325
|
+
/** Delete the bucket (for "user", also its request rows). */
|
|
326
|
+
delete: (ctx, args) => ctx.runMutation(c.lib.deleteBucket, { dimension, value: args.value }),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/** Per-user budgets and controls — sugar over the "user" dimension. */
|
|
296
330
|
get users() {
|
|
297
331
|
const c = this.component;
|
|
298
332
|
return {
|
|
299
|
-
list: (ctx) => ctx.runQuery(c.lib.
|
|
300
|
-
setLimits: (ctx, args) =>
|
|
333
|
+
list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension: "user" }),
|
|
334
|
+
setLimits: (ctx, args) => {
|
|
335
|
+
const { userId, ...limits } = args;
|
|
336
|
+
return ctx.runMutation(c.lib.setBucketLimits, {
|
|
337
|
+
dimension: "user",
|
|
338
|
+
value: userId,
|
|
339
|
+
...limits,
|
|
340
|
+
});
|
|
341
|
+
},
|
|
301
342
|
/** One-time "approve another $X" bump (daily is today-only). */
|
|
302
|
-
bump: (ctx, args) => ctx.runMutation(c.lib.
|
|
343
|
+
bump: (ctx, args) => ctx.runMutation(c.lib.bumpBucket, {
|
|
344
|
+
dimension: "user",
|
|
345
|
+
value: args.userId,
|
|
346
|
+
dailyNanos: args.dailyNanos,
|
|
347
|
+
lifetimeNanos: args.lifetimeNanos,
|
|
348
|
+
}),
|
|
303
349
|
/** Delete a user and all their request rows. */
|
|
304
|
-
delete: (ctx, args) => ctx.runMutation(c.lib.
|
|
350
|
+
delete: (ctx, args) => ctx.runMutation(c.lib.deleteBucket, {
|
|
351
|
+
dimension: "user",
|
|
352
|
+
value: args.userId,
|
|
353
|
+
}),
|
|
305
354
|
};
|
|
306
355
|
}
|
|
307
|
-
/** Per-action (per-feature) budgets. */
|
|
356
|
+
/** Per-action (per-feature) budgets — sugar over the "action" dimension. */
|
|
308
357
|
get actions() {
|
|
309
358
|
const c = this.component;
|
|
310
359
|
return {
|
|
311
|
-
list: (ctx) => ctx.runQuery(c.lib.
|
|
312
|
-
setLimits: (ctx, args) =>
|
|
313
|
-
|
|
360
|
+
list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension: "action" }),
|
|
361
|
+
setLimits: (ctx, args) => {
|
|
362
|
+
const { name, ...limits } = args;
|
|
363
|
+
return ctx.runMutation(c.lib.setBucketLimits, {
|
|
364
|
+
dimension: "action",
|
|
365
|
+
value: name,
|
|
366
|
+
...limits,
|
|
367
|
+
});
|
|
368
|
+
},
|
|
369
|
+
bump: (ctx, args) => ctx.runMutation(c.lib.bumpBucket, {
|
|
370
|
+
dimension: "action",
|
|
371
|
+
value: args.name,
|
|
372
|
+
dailyNanos: args.dailyNanos,
|
|
373
|
+
lifetimeNanos: args.lifetimeNanos,
|
|
374
|
+
}),
|
|
314
375
|
};
|
|
315
376
|
}
|
|
316
377
|
/** The deployment-wide budget and retention config. */
|
|
@@ -20,22 +20,19 @@ import type { FunctionReference } from "convex/server";
|
|
|
20
20
|
*/
|
|
21
21
|
export type ComponentApi<Name extends string | undefined = string | undefined> = {
|
|
22
22
|
lib: {
|
|
23
|
-
|
|
23
|
+
bumpBucket: FunctionReference<"mutation", "internal", {
|
|
24
24
|
dailyNanos?: number;
|
|
25
|
+
dimension: string;
|
|
25
26
|
lifetimeNanos?: number;
|
|
26
|
-
|
|
27
|
+
value: string;
|
|
27
28
|
}, null, Name>;
|
|
28
29
|
bumpGlobal: FunctionReference<"mutation", "internal", {
|
|
29
30
|
dailyNanos?: number;
|
|
30
31
|
lifetimeNanos?: number;
|
|
31
32
|
}, null, Name>;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
userId: string;
|
|
36
|
-
}, null, Name>;
|
|
37
|
-
deleteUser: FunctionReference<"mutation", "internal", {
|
|
38
|
-
userId: string;
|
|
33
|
+
deleteBucket: FunctionReference<"mutation", "internal", {
|
|
34
|
+
dimension: string;
|
|
35
|
+
value: string;
|
|
39
36
|
}, {
|
|
40
37
|
deletedThisBatch: number;
|
|
41
38
|
done: boolean;
|
|
@@ -51,6 +48,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
51
48
|
}, {
|
|
52
49
|
costNanos: number;
|
|
53
50
|
}, Name>;
|
|
51
|
+
getBucket: FunctionReference<"query", "internal", {
|
|
52
|
+
dimension: string;
|
|
53
|
+
value: string;
|
|
54
|
+
}, any, Name>;
|
|
54
55
|
getGlobalStatus: FunctionReference<"query", "internal", {}, {
|
|
55
56
|
dailySpendLimitNanos: number | null;
|
|
56
57
|
enforcement: "hard" | "soft";
|
|
@@ -68,37 +69,30 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
68
69
|
lineage: FunctionReference<"query", "internal", {
|
|
69
70
|
requestId: string;
|
|
70
71
|
}, any, Name>;
|
|
71
|
-
|
|
72
|
+
listBuckets: FunctionReference<"query", "internal", {
|
|
73
|
+
dimension?: string;
|
|
74
|
+
}, any, Name>;
|
|
72
75
|
listPrices: FunctionReference<"query", "internal", {}, any, Name>;
|
|
73
76
|
listRequests: FunctionReference<"query", "internal", {
|
|
74
77
|
limit?: number;
|
|
75
78
|
userId?: string;
|
|
76
79
|
}, any, Name>;
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
setBucketLimits: FunctionReference<"mutation", "internal", {
|
|
81
|
+
blocked?: boolean;
|
|
79
82
|
dailySpendLimitNanos?: number;
|
|
80
83
|
dailyTokenLimit?: number;
|
|
81
|
-
|
|
84
|
+
dimension: string;
|
|
82
85
|
enforcement?: "hard" | "soft";
|
|
83
86
|
lifetimeSpendLimitNanos?: number;
|
|
84
87
|
lifetimeTokenLimit?: number;
|
|
85
|
-
|
|
88
|
+
requestsPerMinute?: number;
|
|
89
|
+
value: string;
|
|
86
90
|
}, null, Name>;
|
|
87
91
|
setGlobalLimits: FunctionReference<"mutation", "internal", {
|
|
88
92
|
dailySpendLimitNanos?: number;
|
|
89
93
|
enforcement?: "hard" | "soft";
|
|
90
94
|
lifetimeSpendLimitNanos?: number;
|
|
91
95
|
}, null, Name>;
|
|
92
|
-
setLimits: FunctionReference<"mutation", "internal", {
|
|
93
|
-
blocked?: boolean;
|
|
94
|
-
dailySpendLimitNanos?: number;
|
|
95
|
-
dailyTokenLimit?: number;
|
|
96
|
-
enforcement?: "hard" | "soft";
|
|
97
|
-
lifetimeSpendLimitNanos?: number;
|
|
98
|
-
lifetimeTokenLimit?: number;
|
|
99
|
-
requestsPerMinute?: number;
|
|
100
|
-
userId: string;
|
|
101
|
-
}, null, Name>;
|
|
102
96
|
setModelPolicy: FunctionReference<"mutation", "internal", {
|
|
103
97
|
mode: "open" | "allowlist" | "denylist";
|
|
104
98
|
models: Array<string>;
|
|
@@ -119,6 +113,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
119
113
|
}>;
|
|
120
114
|
model: string;
|
|
121
115
|
rerunOf?: string;
|
|
116
|
+
tags?: Array<{
|
|
117
|
+
dimension: string;
|
|
118
|
+
value: string;
|
|
119
|
+
}>;
|
|
122
120
|
userId: string;
|
|
123
121
|
}, {
|
|
124
122
|
allowed: true;
|