@convex-dev/ai-budget 0.0.2-alpha.4 → 0.0.2-alpha.5
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 +67 -6
- package/dist/client/index.d.ts +318 -43
- package/dist/client/index.js +115 -68
- package/dist/component/_generated/component.d.ts +32 -0
- package/dist/component/lib.d.ts +73 -0
- package/dist/component/lib.js +304 -32
- package/dist/component/schema.d.ts +76 -3
- package/dist/component/schema.js +56 -2
- package/package.json +1 -1
- package/src/client/index.ts +199 -131
- package/src/component/_generated/component.ts +54 -3
- package/src/component/lib.test.ts +118 -0
- package/src/component/lib.ts +375 -48
- package/src/component/schema.ts +59 -2
package/src/client/index.ts
CHANGED
|
@@ -49,22 +49,32 @@ export type AIGatewayApi = AIBudgetApi;
|
|
|
49
49
|
*/
|
|
50
50
|
export type Tag = { dimension: string; value: string };
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
export type
|
|
52
|
+
/** Common shape for budget-event callbacks. */
|
|
53
|
+
export type BudgetEventInfo = {
|
|
54
54
|
userId: string;
|
|
55
55
|
action?: string;
|
|
56
56
|
tags?: Tag[];
|
|
57
|
-
requestId
|
|
58
|
-
warnings
|
|
57
|
+
requestId?: string;
|
|
58
|
+
/** soft-cap warnings (onSoftLimit) or approaching-cap notices (onThreshold). */
|
|
59
|
+
messages: string[];
|
|
60
|
+
/** rejection code/reason (onLimitReached only). */
|
|
61
|
+
code?: string;
|
|
62
|
+
reason?: string;
|
|
59
63
|
};
|
|
64
|
+
/** @deprecated use BudgetEventInfo */
|
|
65
|
+
export type SoftLimitInfo = BudgetEventInfo & { warnings: string[] };
|
|
60
66
|
export type AIBudgetOptions = {
|
|
61
67
|
defaultModel?: string;
|
|
62
68
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
69
|
+
* A *soft* limit was exceeded (request still allowed). Lets you surface budget
|
|
70
|
+
* warnings even on the languageModel/Agent path where they can't be returned.
|
|
71
|
+
* Errors thrown in any of these callbacks are swallowed.
|
|
66
72
|
*/
|
|
67
73
|
onSoftLimit?: (info: SoftLimitInfo) => void | Promise<void>;
|
|
74
|
+
/** Usage crossed a bucket's warnAtPct threshold (approaching a cap). */
|
|
75
|
+
onThreshold?: (info: BudgetEventInfo) => void | Promise<void>;
|
|
76
|
+
/** A *hard* limit blocked the request (fires just before chat/model throws). */
|
|
77
|
+
onLimitReached?: (info: BudgetEventInfo) => void | Promise<void>;
|
|
68
78
|
};
|
|
69
79
|
|
|
70
80
|
type RunQueryCtx = {
|
|
@@ -123,6 +133,8 @@ export type ChatResult = {
|
|
|
123
133
|
cachedTokens: number;
|
|
124
134
|
/** Soft-limit warnings raised at admission (empty unless a soft cap was hit). */
|
|
125
135
|
warnings: string[];
|
|
136
|
+
/** Approaching-cap notices (empty unless a warnAtPct threshold was crossed). */
|
|
137
|
+
notices: string[];
|
|
126
138
|
};
|
|
127
139
|
|
|
128
140
|
// ---------- helpers ----------
|
|
@@ -143,10 +155,12 @@ function extractUsage(usage: any): {
|
|
|
143
155
|
return {
|
|
144
156
|
promptTokens: toTokenCount(usage?.inputTokens ?? usage?.promptTokens),
|
|
145
157
|
completionTokens: toTokenCount(usage?.outputTokens ?? usage?.completionTokens),
|
|
146
|
-
// cached prompt tokens
|
|
147
|
-
// `
|
|
158
|
+
// cached prompt tokens. The Convex gateway reports these at
|
|
159
|
+
// `usage.inputTokenDetails.cacheReadTokens`; the other paths cover AI SDK v5
|
|
160
|
+
// (`cachedInputTokens`) and raw OpenAI-compatible shapes.
|
|
148
161
|
cachedTokens: toTokenCount(
|
|
149
|
-
usage?.
|
|
162
|
+
usage?.inputTokenDetails?.cacheReadTokens ??
|
|
163
|
+
usage?.cachedInputTokens ??
|
|
150
164
|
usage?.promptTokensDetails?.cachedTokens ??
|
|
151
165
|
usage?.prompt_tokens_details?.cached_tokens ??
|
|
152
166
|
usage?.cached_tokens
|
|
@@ -154,6 +168,20 @@ function extractUsage(usage: any): {
|
|
|
154
168
|
};
|
|
155
169
|
}
|
|
156
170
|
|
|
171
|
+
// The gateway doesn't report a dollar cost today, but if a future response ever
|
|
172
|
+
// carries an unambiguous nanodollar cost we pass it straight through as
|
|
173
|
+
// authoritative (finishRequest prefers it over the token-based estimate). Only
|
|
174
|
+
// an explicitly nano-denominated field is trusted — a bare `cost` could be in
|
|
175
|
+
// dollars and silently mis-bill by 1e9×.
|
|
176
|
+
function extractGatewayCostNanos(result: any): number | undefined {
|
|
177
|
+
const meta = result?.providerMetadata?.convexGateway;
|
|
178
|
+
const candidates = [meta?.costNanos, result?.usage?.costNanos];
|
|
179
|
+
for (const c of candidates) {
|
|
180
|
+
if (typeof c === "number" && Number.isFinite(c) && c >= 0) return c;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
// Flatten an AI SDK prompt (roles + content parts) into simple storable messages.
|
|
158
186
|
function simplifyPrompt(prompt: any): Message[] {
|
|
159
187
|
if (!Array.isArray(prompt)) return [];
|
|
@@ -187,23 +215,71 @@ function extractText(result: any): string {
|
|
|
187
215
|
|
|
188
216
|
// ---------- client ----------
|
|
189
217
|
|
|
218
|
+
/** Limits/controls settable on any budget bucket (user, action, or tag). */
|
|
219
|
+
export type BucketLimits = {
|
|
220
|
+
requestsPerMinute?: number;
|
|
221
|
+
maxConcurrent?: number;
|
|
222
|
+
dailySpendLimitNanos?: number;
|
|
223
|
+
monthlySpendLimitNanos?: number;
|
|
224
|
+
lifetimeSpendLimitNanos?: number;
|
|
225
|
+
dailyTokenLimit?: number;
|
|
226
|
+
monthlyTokenLimit?: number;
|
|
227
|
+
lifetimeTokenLimit?: number;
|
|
228
|
+
/** Fire an approaching-limit alert at this fraction of a cap (e.g. 0.8). */
|
|
229
|
+
warnAtPct?: number;
|
|
230
|
+
enforcement?: "hard" | "soft";
|
|
231
|
+
blocked?: boolean;
|
|
232
|
+
};
|
|
233
|
+
/** One-time bump amounts, added on top of a standing cap. */
|
|
234
|
+
export type BumpArgs = {
|
|
235
|
+
dailyNanos?: number;
|
|
236
|
+
monthlyNanos?: number;
|
|
237
|
+
lifetimeNanos?: number;
|
|
238
|
+
};
|
|
239
|
+
|
|
190
240
|
export class AIBudget {
|
|
191
241
|
public defaultModel: string;
|
|
192
242
|
private onSoftLimit?: AIBudgetOptions["onSoftLimit"];
|
|
243
|
+
private onThreshold?: AIBudgetOptions["onThreshold"];
|
|
244
|
+
private onLimitReached?: AIBudgetOptions["onLimitReached"];
|
|
193
245
|
constructor(
|
|
194
246
|
public component: AIBudgetApi,
|
|
195
247
|
options?: AIBudgetOptions
|
|
196
248
|
) {
|
|
197
249
|
this.defaultModel = options?.defaultModel ?? "openai/gpt-4o-mini";
|
|
198
250
|
this.onSoftLimit = options?.onSoftLimit;
|
|
251
|
+
this.onThreshold = options?.onThreshold;
|
|
252
|
+
this.onLimitReached = options?.onLimitReached;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fire the soft-limit + threshold callbacks from a startRequest result.
|
|
256
|
+
private async fireBudgetEvents(
|
|
257
|
+
base: Omit<BudgetEventInfo, "messages">,
|
|
258
|
+
warnings: string[],
|
|
259
|
+
notices: string[]
|
|
260
|
+
) {
|
|
261
|
+
if (warnings.length > 0 && this.onSoftLimit) {
|
|
262
|
+
try {
|
|
263
|
+
await this.onSoftLimit({ ...base, messages: warnings, warnings });
|
|
264
|
+
} catch {
|
|
265
|
+
/* never let a callback error break a request */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (notices.length > 0 && this.onThreshold) {
|
|
269
|
+
try {
|
|
270
|
+
await this.onThreshold({ ...base, messages: notices });
|
|
271
|
+
} catch {
|
|
272
|
+
/* swallow */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
199
275
|
}
|
|
200
276
|
|
|
201
|
-
private async
|
|
202
|
-
if (
|
|
277
|
+
private async fireLimitReached(info: BudgetEventInfo) {
|
|
278
|
+
if (!this.onLimitReached) return;
|
|
203
279
|
try {
|
|
204
|
-
await this.
|
|
280
|
+
await this.onLimitReached(info);
|
|
205
281
|
} catch {
|
|
206
|
-
|
|
282
|
+
/* swallow */
|
|
207
283
|
}
|
|
208
284
|
}
|
|
209
285
|
|
|
@@ -240,6 +316,14 @@ export class AIBudget {
|
|
|
240
316
|
rerunOf: args.rerunOf as any,
|
|
241
317
|
});
|
|
242
318
|
if (!started.allowed) {
|
|
319
|
+
await this.fireLimitReached({
|
|
320
|
+
userId,
|
|
321
|
+
action: actionName,
|
|
322
|
+
tags: args.tags,
|
|
323
|
+
messages: [started.reason],
|
|
324
|
+
code: started.code,
|
|
325
|
+
reason: started.reason,
|
|
326
|
+
});
|
|
243
327
|
throw new ConvexError({
|
|
244
328
|
kind: "AIBudgetLimit",
|
|
245
329
|
code: started.code,
|
|
@@ -248,13 +332,12 @@ export class AIBudget {
|
|
|
248
332
|
}
|
|
249
333
|
const requestId = started.requestId;
|
|
250
334
|
const warnings = started.warnings;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
action: actionName,
|
|
254
|
-
tags: args.tags,
|
|
255
|
-
requestId,
|
|
335
|
+
const notices = started.notices;
|
|
336
|
+
await this.fireBudgetEvents(
|
|
337
|
+
{ userId, action: actionName, tags: args.tags, requestId },
|
|
256
338
|
warnings,
|
|
257
|
-
|
|
339
|
+
notices
|
|
340
|
+
);
|
|
258
341
|
const start = Date.now();
|
|
259
342
|
try {
|
|
260
343
|
// The full chain (incl. system) is stored on the request for audit/replay,
|
|
@@ -277,10 +360,11 @@ export class AIBudget {
|
|
|
277
360
|
requestId,
|
|
278
361
|
responseText: result.text,
|
|
279
362
|
...usage,
|
|
363
|
+
costNanos: extractGatewayCostNanos(result),
|
|
280
364
|
latencyMs: Date.now() - start,
|
|
281
365
|
}
|
|
282
366
|
);
|
|
283
|
-
return { text: result.text, requestId, costNanos, warnings, ...usage };
|
|
367
|
+
return { text: result.text, requestId, costNanos, warnings, notices, ...usage };
|
|
284
368
|
} catch (e) {
|
|
285
369
|
await ctx.runMutation(this.component.lib.finishRequest, {
|
|
286
370
|
requestId,
|
|
@@ -309,11 +393,13 @@ export class AIBudget {
|
|
|
309
393
|
): LanguageModel {
|
|
310
394
|
const modelId = opts.model ?? this.defaultModel;
|
|
311
395
|
const component = this.component;
|
|
312
|
-
const
|
|
396
|
+
const fireBudgetEvents = this.fireBudgetEvents.bind(this);
|
|
397
|
+
const fireLimitReached = this.fireLimitReached.bind(this);
|
|
313
398
|
|
|
314
399
|
const begin = async (params: any) => {
|
|
315
400
|
const userId = await resolveUserId(ctx, opts.userId);
|
|
316
401
|
const actionName = await resolveActionName(ctx, opts.action);
|
|
402
|
+
const base = { userId, action: actionName, tags: opts.tags };
|
|
317
403
|
const started = await ctx.runMutation(component.lib.startRequest, {
|
|
318
404
|
userId,
|
|
319
405
|
actionName,
|
|
@@ -322,19 +408,23 @@ export class AIBudget {
|
|
|
322
408
|
messages: simplifyPrompt(params.prompt),
|
|
323
409
|
});
|
|
324
410
|
if (!started.allowed) {
|
|
411
|
+
await fireLimitReached({
|
|
412
|
+
...base,
|
|
413
|
+
messages: [started.reason],
|
|
414
|
+
code: started.code,
|
|
415
|
+
reason: started.reason,
|
|
416
|
+
});
|
|
325
417
|
throw new ConvexError({
|
|
326
418
|
kind: "AIBudgetLimit",
|
|
327
419
|
code: started.code,
|
|
328
420
|
reason: started.reason,
|
|
329
421
|
});
|
|
330
422
|
}
|
|
331
|
-
await
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
warnings: started.warnings,
|
|
337
|
-
});
|
|
423
|
+
await fireBudgetEvents(
|
|
424
|
+
{ ...base, requestId: started.requestId },
|
|
425
|
+
started.warnings,
|
|
426
|
+
started.notices
|
|
427
|
+
);
|
|
338
428
|
return started.requestId;
|
|
339
429
|
};
|
|
340
430
|
const finish = async (
|
|
@@ -344,6 +434,8 @@ export class AIBudget {
|
|
|
344
434
|
error?: string;
|
|
345
435
|
promptTokens?: number;
|
|
346
436
|
completionTokens?: number;
|
|
437
|
+
cachedTokens?: number;
|
|
438
|
+
costNanos?: number;
|
|
347
439
|
latencyMs?: number;
|
|
348
440
|
}
|
|
349
441
|
) => ctx.runMutation(component.lib.finishRequest, { requestId, ...fields });
|
|
@@ -359,6 +451,7 @@ export class AIBudget {
|
|
|
359
451
|
await finish(requestId, {
|
|
360
452
|
responseText: extractText(result),
|
|
361
453
|
...extractUsage(result.usage),
|
|
454
|
+
costNanos: extractGatewayCostNanos(result),
|
|
362
455
|
latencyMs: Date.now() - start,
|
|
363
456
|
});
|
|
364
457
|
return result;
|
|
@@ -445,8 +538,16 @@ export class AIBudget {
|
|
|
445
538
|
get requests() {
|
|
446
539
|
const c = this.component;
|
|
447
540
|
return {
|
|
448
|
-
|
|
449
|
-
|
|
541
|
+
/** Filter by userId, or by any {dimension, value} (incl. custom tags). */
|
|
542
|
+
list: (
|
|
543
|
+
ctx: RunQueryCtx,
|
|
544
|
+
args: {
|
|
545
|
+
userId?: string;
|
|
546
|
+
dimension?: string;
|
|
547
|
+
value?: string;
|
|
548
|
+
limit?: number;
|
|
549
|
+
} = {}
|
|
550
|
+
) => ctx.runQuery(c.lib.listRequests, args),
|
|
450
551
|
/** Ancestors up to the original, plus direct re-runs. */
|
|
451
552
|
lineage: (ctx: RunQueryCtx, args: { requestId: string }) =>
|
|
452
553
|
ctx.runQuery(c.lib.lineage, { requestId: args.requestId as any }),
|
|
@@ -463,130 +564,92 @@ export class AIBudget {
|
|
|
463
564
|
* generalization of `users`/`actions`. Give it any dimension name (team,
|
|
464
565
|
* project, tenant, customer, env, feature, …) and set caps per value:
|
|
465
566
|
*
|
|
466
|
-
* ai.tag("customer").setLimits(ctx, { value: "acme",
|
|
467
|
-
* ai.tag("customer").
|
|
567
|
+
* ai.tag("customer").setLimits(ctx, { value: "acme", monthlySpendLimitNanos });
|
|
568
|
+
* ai.tag("customer").history(ctx, { value: "acme", period: "day" });
|
|
468
569
|
*
|
|
469
570
|
* Attribute a call to it by passing `tags` to `chat`/`languageModel`.
|
|
470
571
|
*/
|
|
471
572
|
tag(dimension: string) {
|
|
472
|
-
|
|
473
|
-
return {
|
|
474
|
-
/** All buckets in this dimension. */
|
|
475
|
-
list: (ctx: RunQueryCtx) =>
|
|
476
|
-
ctx.runQuery(c.lib.listBuckets, { dimension }),
|
|
477
|
-
/** One bucket's limits + spend (null if it has none yet). */
|
|
478
|
-
get: (ctx: RunQueryCtx, args: { value: string }) =>
|
|
479
|
-
ctx.runQuery(c.lib.getBucket, { dimension, value: args.value }),
|
|
480
|
-
setLimits: (
|
|
481
|
-
ctx: RunMutationCtx,
|
|
482
|
-
args: {
|
|
483
|
-
value: string;
|
|
484
|
-
requestsPerMinute?: number;
|
|
485
|
-
dailySpendLimitNanos?: number;
|
|
486
|
-
lifetimeSpendLimitNanos?: number;
|
|
487
|
-
dailyTokenLimit?: number;
|
|
488
|
-
lifetimeTokenLimit?: number;
|
|
489
|
-
enforcement?: "hard" | "soft";
|
|
490
|
-
blocked?: boolean;
|
|
491
|
-
}
|
|
492
|
-
) => ctx.runMutation(c.lib.setBucketLimits, { dimension, ...args }),
|
|
493
|
-
/** One-time "approve another $X" bump (daily is today-only). */
|
|
494
|
-
bump: (
|
|
495
|
-
ctx: RunMutationCtx,
|
|
496
|
-
args: { value: string; dailyNanos?: number; lifetimeNanos?: number }
|
|
497
|
-
) => ctx.runMutation(c.lib.bumpBucket, { dimension, ...args }),
|
|
498
|
-
/** Delete the bucket (for "user", also its request rows). */
|
|
499
|
-
delete: (ctx: RunMutationCtx, args: { value: string }) =>
|
|
500
|
-
ctx.runMutation(c.lib.deleteBucket, { dimension, value: args.value }),
|
|
501
|
-
};
|
|
573
|
+
return this.dimensionApi(dimension, (a: { value: string }) => a.value);
|
|
502
574
|
}
|
|
503
575
|
|
|
504
|
-
|
|
505
|
-
|
|
576
|
+
// Shared implementation behind tag()/users/actions. `key` maps the namespace's
|
|
577
|
+
// id field (value/userId/name) to the bucket value.
|
|
578
|
+
private dimensionApi<A extends Record<string, any>>(
|
|
579
|
+
dimension: string,
|
|
580
|
+
key: (a: A) => string
|
|
581
|
+
) {
|
|
506
582
|
const c = this.component;
|
|
507
583
|
return {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
dailySpendLimitNanos?: number;
|
|
516
|
-
lifetimeSpendLimitNanos?: number;
|
|
517
|
-
dailyTokenLimit?: number;
|
|
518
|
-
lifetimeTokenLimit?: number;
|
|
519
|
-
enforcement?: "hard" | "soft";
|
|
520
|
-
blocked?: boolean;
|
|
521
|
-
}
|
|
522
|
-
) => {
|
|
523
|
-
const { userId, ...limits } = args;
|
|
584
|
+
/** All buckets in this dimension. */
|
|
585
|
+
list: (ctx: RunQueryCtx) => ctx.runQuery(c.lib.listBuckets, { dimension }),
|
|
586
|
+
/** One bucket's limits + spend (null if it has none yet). */
|
|
587
|
+
get: (ctx: RunQueryCtx, args: A) =>
|
|
588
|
+
ctx.runQuery(c.lib.getBucket, { dimension, value: key(args) }),
|
|
589
|
+
setLimits: (ctx: RunMutationCtx, args: A & BucketLimits) => {
|
|
590
|
+
const { value, userId, name, ...limits } = args as any;
|
|
524
591
|
return ctx.runMutation(c.lib.setBucketLimits, {
|
|
525
|
-
dimension
|
|
526
|
-
value:
|
|
592
|
+
dimension,
|
|
593
|
+
value: key(args),
|
|
527
594
|
...limits,
|
|
528
595
|
});
|
|
529
596
|
},
|
|
530
|
-
/** One-time "approve another $X" bump (daily
|
|
531
|
-
bump: (
|
|
532
|
-
ctx: RunMutationCtx,
|
|
533
|
-
args: { userId: string; dailyNanos?: number; lifetimeNanos?: number }
|
|
534
|
-
) =>
|
|
597
|
+
/** One-time "approve another $X" bump (daily/monthly reset with the window). */
|
|
598
|
+
bump: (ctx: RunMutationCtx, args: A & BumpArgs) =>
|
|
535
599
|
ctx.runMutation(c.lib.bumpBucket, {
|
|
536
|
-
dimension
|
|
537
|
-
value: args
|
|
600
|
+
dimension,
|
|
601
|
+
value: key(args),
|
|
538
602
|
dailyNanos: args.dailyNanos,
|
|
603
|
+
monthlyNanos: args.monthlyNanos,
|
|
539
604
|
lifetimeNanos: args.lifetimeNanos,
|
|
540
605
|
}),
|
|
541
|
-
/**
|
|
542
|
-
|
|
543
|
-
ctx
|
|
544
|
-
|
|
545
|
-
|
|
606
|
+
/** Manually credit (negative) or debit (positive) this bucket. */
|
|
607
|
+
adjust: (
|
|
608
|
+
ctx: RunMutationCtx,
|
|
609
|
+
args: A & { deltaNanos: number; tokens?: number; reason?: string }
|
|
610
|
+
) =>
|
|
611
|
+
ctx.runMutation(c.lib.adjustBucket, {
|
|
612
|
+
dimension,
|
|
613
|
+
value: key(args),
|
|
614
|
+
deltaNanos: args.deltaNanos,
|
|
615
|
+
tokens: args.tokens,
|
|
616
|
+
reason: args.reason,
|
|
546
617
|
}),
|
|
618
|
+
/** Durable spend history for this bucket (per day or per month). */
|
|
619
|
+
history: (
|
|
620
|
+
ctx: RunQueryCtx,
|
|
621
|
+
args: A & { period?: "day" | "month"; limit?: number }
|
|
622
|
+
) =>
|
|
623
|
+
ctx.runQuery(c.lib.usageHistory, {
|
|
624
|
+
dimension,
|
|
625
|
+
value: key(args),
|
|
626
|
+
period: args.period ?? "day",
|
|
627
|
+
limit: args.limit,
|
|
628
|
+
}),
|
|
629
|
+
/** Manual-adjustment audit log for this bucket. */
|
|
630
|
+
adjustments: (ctx: RunQueryCtx, args: A & { limit?: number }) =>
|
|
631
|
+
ctx.runQuery(c.lib.listAdjustments, {
|
|
632
|
+
dimension,
|
|
633
|
+
value: key(args),
|
|
634
|
+
limit: args.limit,
|
|
635
|
+
}),
|
|
636
|
+
/** Delete the bucket (for "user", also its request rows). */
|
|
637
|
+
delete: (ctx: RunMutationCtx, args: A) =>
|
|
638
|
+
ctx.runMutation(c.lib.deleteBucket, { dimension, value: key(args) }),
|
|
547
639
|
};
|
|
548
640
|
}
|
|
549
641
|
|
|
642
|
+
/** Per-user budgets and controls — sugar over the "user" dimension. */
|
|
643
|
+
get users() {
|
|
644
|
+
return this.dimensionApi<{ userId: string }>("user", (a) => a.userId);
|
|
645
|
+
}
|
|
646
|
+
|
|
550
647
|
/** Per-action (per-feature) budgets — sugar over the "action" dimension. */
|
|
551
648
|
get actions() {
|
|
552
|
-
|
|
553
|
-
return {
|
|
554
|
-
list: (ctx: RunQueryCtx) =>
|
|
555
|
-
ctx.runQuery(c.lib.listBuckets, { dimension: "action" }),
|
|
556
|
-
setLimits: (
|
|
557
|
-
ctx: RunMutationCtx,
|
|
558
|
-
args: {
|
|
559
|
-
name: string;
|
|
560
|
-
dailySpendLimitNanos?: number;
|
|
561
|
-
lifetimeSpendLimitNanos?: number;
|
|
562
|
-
dailyTokenLimit?: number;
|
|
563
|
-
lifetimeTokenLimit?: number;
|
|
564
|
-
enforcement?: "hard" | "soft";
|
|
565
|
-
/** Hard-block this action (was `disabled`). */
|
|
566
|
-
blocked?: boolean;
|
|
567
|
-
}
|
|
568
|
-
) => {
|
|
569
|
-
const { name, ...limits } = args;
|
|
570
|
-
return ctx.runMutation(c.lib.setBucketLimits, {
|
|
571
|
-
dimension: "action",
|
|
572
|
-
value: name,
|
|
573
|
-
...limits,
|
|
574
|
-
});
|
|
575
|
-
},
|
|
576
|
-
bump: (
|
|
577
|
-
ctx: RunMutationCtx,
|
|
578
|
-
args: { name: string; dailyNanos?: number; lifetimeNanos?: number }
|
|
579
|
-
) =>
|
|
580
|
-
ctx.runMutation(c.lib.bumpBucket, {
|
|
581
|
-
dimension: "action",
|
|
582
|
-
value: args.name,
|
|
583
|
-
dailyNanos: args.dailyNanos,
|
|
584
|
-
lifetimeNanos: args.lifetimeNanos,
|
|
585
|
-
}),
|
|
586
|
-
};
|
|
649
|
+
return this.dimensionApi<{ name: string }>("action", (a) => a.name);
|
|
587
650
|
}
|
|
588
651
|
|
|
589
|
-
/** The deployment-wide budget and retention config. */
|
|
652
|
+
/** The deployment-wide budget, alerts, and retention config. */
|
|
590
653
|
get global() {
|
|
591
654
|
const c = this.component;
|
|
592
655
|
return {
|
|
@@ -605,6 +668,9 @@ export class AIBudget {
|
|
|
605
668
|
ctx: RunMutationCtx,
|
|
606
669
|
args: { dailyNanos?: number; lifetimeNanos?: number }
|
|
607
670
|
) => ctx.runMutation(c.lib.bumpGlobal, args),
|
|
671
|
+
/** Default approaching-limit alert threshold (fraction of a cap, e.g. 0.8). */
|
|
672
|
+
setAlertDefaults: (ctx: RunMutationCtx, args: { warnAtPct?: number }) =>
|
|
673
|
+
ctx.runMutation(c.lib.setAlertDefaults, args),
|
|
608
674
|
/** Request-row retention window in ms (default 1h; 0 disables). */
|
|
609
675
|
setRetention: (ctx: RunMutationCtx, args: { retentionMs: number }) =>
|
|
610
676
|
ctx.runMutation(c.lib.setRetention, args),
|
|
@@ -635,6 +701,8 @@ export class AIBudget {
|
|
|
635
701
|
model: string;
|
|
636
702
|
inputNanosPerMTok: number;
|
|
637
703
|
outputNanosPerMTok: number;
|
|
704
|
+
/** Cache-read rate; defaults to a discount off input if omitted. */
|
|
705
|
+
cachedNanosPerMTok?: number;
|
|
638
706
|
}
|
|
639
707
|
) => ctx.runMutation(c.lib.setPrice, args),
|
|
640
708
|
};
|
|
@@ -24,6 +24,19 @@ import type { FunctionReference } from "convex/server";
|
|
|
24
24
|
export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
25
25
|
{
|
|
26
26
|
lib: {
|
|
27
|
+
adjustBucket: FunctionReference<
|
|
28
|
+
"mutation",
|
|
29
|
+
"internal",
|
|
30
|
+
{
|
|
31
|
+
deltaNanos: number;
|
|
32
|
+
dimension: string;
|
|
33
|
+
reason?: string;
|
|
34
|
+
tokens?: number;
|
|
35
|
+
value: string;
|
|
36
|
+
},
|
|
37
|
+
null,
|
|
38
|
+
Name
|
|
39
|
+
>;
|
|
27
40
|
bumpBucket: FunctionReference<
|
|
28
41
|
"mutation",
|
|
29
42
|
"internal",
|
|
@@ -31,6 +44,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
31
44
|
dailyNanos?: number;
|
|
32
45
|
dimension: string;
|
|
33
46
|
lifetimeNanos?: number;
|
|
47
|
+
monthlyNanos?: number;
|
|
34
48
|
value: string;
|
|
35
49
|
},
|
|
36
50
|
null,
|
|
@@ -39,7 +53,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
39
53
|
bumpGlobal: FunctionReference<
|
|
40
54
|
"mutation",
|
|
41
55
|
"internal",
|
|
42
|
-
{ dailyNanos?: number; lifetimeNanos?: number },
|
|
56
|
+
{ dailyNanos?: number; lifetimeNanos?: number; monthlyNanos?: number },
|
|
43
57
|
null,
|
|
44
58
|
Name
|
|
45
59
|
>;
|
|
@@ -56,6 +70,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
56
70
|
{
|
|
57
71
|
cachedTokens?: number;
|
|
58
72
|
completionTokens?: number;
|
|
73
|
+
costNanos?: number;
|
|
59
74
|
error?: string;
|
|
60
75
|
latencyMs?: number;
|
|
61
76
|
promptTokens?: number;
|
|
@@ -106,6 +121,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
106
121
|
any,
|
|
107
122
|
Name
|
|
108
123
|
>;
|
|
124
|
+
listAdjustments: FunctionReference<
|
|
125
|
+
"query",
|
|
126
|
+
"internal",
|
|
127
|
+
{ dimension: string; limit?: number; value: string },
|
|
128
|
+
any,
|
|
129
|
+
Name
|
|
130
|
+
>;
|
|
109
131
|
listBuckets: FunctionReference<
|
|
110
132
|
"query",
|
|
111
133
|
"internal",
|
|
@@ -117,10 +139,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
117
139
|
listRequests: FunctionReference<
|
|
118
140
|
"query",
|
|
119
141
|
"internal",
|
|
120
|
-
{ limit?: number; userId?: string },
|
|
142
|
+
{ dimension?: string; limit?: number; userId?: string; value?: string },
|
|
121
143
|
any,
|
|
122
144
|
Name
|
|
123
145
|
>;
|
|
146
|
+
setAlertDefaults: FunctionReference<
|
|
147
|
+
"mutation",
|
|
148
|
+
"internal",
|
|
149
|
+
{ warnAtPct?: number },
|
|
150
|
+
null,
|
|
151
|
+
Name
|
|
152
|
+
>;
|
|
124
153
|
setBucketLimits: FunctionReference<
|
|
125
154
|
"mutation",
|
|
126
155
|
"internal",
|
|
@@ -132,8 +161,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
132
161
|
enforcement?: "hard" | "soft";
|
|
133
162
|
lifetimeSpendLimitNanos?: number;
|
|
134
163
|
lifetimeTokenLimit?: number;
|
|
164
|
+
maxConcurrent?: number;
|
|
165
|
+
monthlySpendLimitNanos?: number;
|
|
166
|
+
monthlyTokenLimit?: number;
|
|
135
167
|
requestsPerMinute?: number;
|
|
136
168
|
value: string;
|
|
169
|
+
warnAtPct?: number;
|
|
137
170
|
},
|
|
138
171
|
null,
|
|
139
172
|
Name
|
|
@@ -160,6 +193,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
160
193
|
"mutation",
|
|
161
194
|
"internal",
|
|
162
195
|
{
|
|
196
|
+
cachedNanosPerMTok?: number;
|
|
163
197
|
inputNanosPerMTok: number;
|
|
164
198
|
model: string;
|
|
165
199
|
outputNanosPerMTok: number;
|
|
@@ -185,9 +219,26 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
185
219
|
tags?: Array<{ dimension: string; value: string }>;
|
|
186
220
|
userId: string;
|
|
187
221
|
},
|
|
188
|
-
| {
|
|
222
|
+
| {
|
|
223
|
+
allowed: true;
|
|
224
|
+
notices: Array<string>;
|
|
225
|
+
requestId: string;
|
|
226
|
+
warnings: Array<string>;
|
|
227
|
+
}
|
|
189
228
|
| { allowed: false; code: string; reason: string },
|
|
190
229
|
Name
|
|
191
230
|
>;
|
|
231
|
+
usageHistory: FunctionReference<
|
|
232
|
+
"query",
|
|
233
|
+
"internal",
|
|
234
|
+
{
|
|
235
|
+
dimension: string;
|
|
236
|
+
limit?: number;
|
|
237
|
+
period: "day" | "month";
|
|
238
|
+
value: string;
|
|
239
|
+
},
|
|
240
|
+
any,
|
|
241
|
+
Name
|
|
242
|
+
>;
|
|
192
243
|
};
|
|
193
244
|
};
|