@convex-dev/ai-budget 0.0.2-alpha.3 → 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 +116 -17
- package/dist/client/index.d.ts +433 -34
- package/dist/client/index.js +141 -33
- package/dist/component/_generated/component.d.ts +54 -24
- package/dist/component/lib.d.ts +130 -40
- package/dist/component/lib.js +550 -290
- package/dist/component/schema.d.ts +103 -53
- package/dist/component/schema.js +81 -38
- package/package.json +1 -1
- package/src/client/index.ts +241 -71
- package/src/component/_generated/component.ts +77 -29
- package/src/component/lib.test.ts +167 -15
- package/src/component/lib.ts +640 -313
- package/src/component/schema.ts +84 -38
package/src/client/index.ts
CHANGED
|
@@ -41,21 +41,40 @@ export type AIBudgetApi = UseApi<typeof api>;
|
|
|
41
41
|
/** @deprecated use AIBudgetApi */
|
|
42
42
|
export type AIGatewayApi = AIBudgetApi;
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* One attribution tag: a (dimension, value) pair, e.g. {dimension:"customer",
|
|
46
|
+
* value:"acme"}. `user` and `action` are built-in dimensions (set via
|
|
47
|
+
* userId/action); use tags for anything else — team, project, tenant, env, ….
|
|
48
|
+
* Any tagged bucket can carry its own budget (see `ai.tag(dimension)`).
|
|
49
|
+
*/
|
|
50
|
+
export type Tag = { dimension: string; value: string };
|
|
51
|
+
|
|
52
|
+
/** Common shape for budget-event callbacks. */
|
|
53
|
+
export type BudgetEventInfo = {
|
|
46
54
|
userId: string;
|
|
47
55
|
action?: string;
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
tags?: Tag[];
|
|
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;
|
|
50
63
|
};
|
|
64
|
+
/** @deprecated use BudgetEventInfo */
|
|
65
|
+
export type SoftLimitInfo = BudgetEventInfo & { warnings: string[] };
|
|
51
66
|
export type AIBudgetOptions = {
|
|
52
67
|
defaultModel?: string;
|
|
53
68
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
57
72
|
*/
|
|
58
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>;
|
|
59
78
|
};
|
|
60
79
|
|
|
61
80
|
type RunQueryCtx = {
|
|
@@ -114,6 +133,8 @@ export type ChatResult = {
|
|
|
114
133
|
cachedTokens: number;
|
|
115
134
|
/** Soft-limit warnings raised at admission (empty unless a soft cap was hit). */
|
|
116
135
|
warnings: string[];
|
|
136
|
+
/** Approaching-cap notices (empty unless a warnAtPct threshold was crossed). */
|
|
137
|
+
notices: string[];
|
|
117
138
|
};
|
|
118
139
|
|
|
119
140
|
// ---------- helpers ----------
|
|
@@ -134,10 +155,12 @@ function extractUsage(usage: any): {
|
|
|
134
155
|
return {
|
|
135
156
|
promptTokens: toTokenCount(usage?.inputTokens ?? usage?.promptTokens),
|
|
136
157
|
completionTokens: toTokenCount(usage?.outputTokens ?? usage?.completionTokens),
|
|
137
|
-
// cached prompt tokens
|
|
138
|
-
// `
|
|
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.
|
|
139
161
|
cachedTokens: toTokenCount(
|
|
140
|
-
usage?.
|
|
162
|
+
usage?.inputTokenDetails?.cacheReadTokens ??
|
|
163
|
+
usage?.cachedInputTokens ??
|
|
141
164
|
usage?.promptTokensDetails?.cachedTokens ??
|
|
142
165
|
usage?.prompt_tokens_details?.cached_tokens ??
|
|
143
166
|
usage?.cached_tokens
|
|
@@ -145,6 +168,20 @@ function extractUsage(usage: any): {
|
|
|
145
168
|
};
|
|
146
169
|
}
|
|
147
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
|
+
|
|
148
185
|
// Flatten an AI SDK prompt (roles + content parts) into simple storable messages.
|
|
149
186
|
function simplifyPrompt(prompt: any): Message[] {
|
|
150
187
|
if (!Array.isArray(prompt)) return [];
|
|
@@ -178,23 +215,71 @@ function extractText(result: any): string {
|
|
|
178
215
|
|
|
179
216
|
// ---------- client ----------
|
|
180
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
|
+
|
|
181
240
|
export class AIBudget {
|
|
182
241
|
public defaultModel: string;
|
|
183
242
|
private onSoftLimit?: AIBudgetOptions["onSoftLimit"];
|
|
243
|
+
private onThreshold?: AIBudgetOptions["onThreshold"];
|
|
244
|
+
private onLimitReached?: AIBudgetOptions["onLimitReached"];
|
|
184
245
|
constructor(
|
|
185
246
|
public component: AIBudgetApi,
|
|
186
247
|
options?: AIBudgetOptions
|
|
187
248
|
) {
|
|
188
249
|
this.defaultModel = options?.defaultModel ?? "openai/gpt-4o-mini";
|
|
189
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
|
+
}
|
|
190
275
|
}
|
|
191
276
|
|
|
192
|
-
private async
|
|
193
|
-
if (
|
|
277
|
+
private async fireLimitReached(info: BudgetEventInfo) {
|
|
278
|
+
if (!this.onLimitReached) return;
|
|
194
279
|
try {
|
|
195
|
-
await this.
|
|
280
|
+
await this.onLimitReached(info);
|
|
196
281
|
} catch {
|
|
197
|
-
|
|
282
|
+
/* swallow */
|
|
198
283
|
}
|
|
199
284
|
}
|
|
200
285
|
|
|
@@ -213,6 +298,8 @@ export class AIBudget {
|
|
|
213
298
|
rerunOf?: string;
|
|
214
299
|
/** Attribute spend to this action name. Defaults to the calling Convex action. */
|
|
215
300
|
action?: string;
|
|
301
|
+
/** Extra attribution dimensions to bill/limit (team, customer, env, …). */
|
|
302
|
+
tags?: Tag[];
|
|
216
303
|
} = {}
|
|
217
304
|
): Promise<ChatResult> {
|
|
218
305
|
const model = args.model ?? this.defaultModel;
|
|
@@ -223,11 +310,20 @@ export class AIBudget {
|
|
|
223
310
|
const started = await ctx.runMutation(this.component.lib.startRequest, {
|
|
224
311
|
userId,
|
|
225
312
|
actionName,
|
|
313
|
+
tags: args.tags,
|
|
226
314
|
model,
|
|
227
315
|
messages,
|
|
228
316
|
rerunOf: args.rerunOf as any,
|
|
229
317
|
});
|
|
230
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
|
+
});
|
|
231
327
|
throw new ConvexError({
|
|
232
328
|
kind: "AIBudgetLimit",
|
|
233
329
|
code: started.code,
|
|
@@ -236,7 +332,12 @@ export class AIBudget {
|
|
|
236
332
|
}
|
|
237
333
|
const requestId = started.requestId;
|
|
238
334
|
const warnings = started.warnings;
|
|
239
|
-
|
|
335
|
+
const notices = started.notices;
|
|
336
|
+
await this.fireBudgetEvents(
|
|
337
|
+
{ userId, action: actionName, tags: args.tags, requestId },
|
|
338
|
+
warnings,
|
|
339
|
+
notices
|
|
340
|
+
);
|
|
240
341
|
const start = Date.now();
|
|
241
342
|
try {
|
|
242
343
|
// The full chain (incl. system) is stored on the request for audit/replay,
|
|
@@ -259,10 +360,11 @@ export class AIBudget {
|
|
|
259
360
|
requestId,
|
|
260
361
|
responseText: result.text,
|
|
261
362
|
...usage,
|
|
363
|
+
costNanos: extractGatewayCostNanos(result),
|
|
262
364
|
latencyMs: Date.now() - start,
|
|
263
365
|
}
|
|
264
366
|
);
|
|
265
|
-
return { text: result.text, requestId, costNanos, warnings, ...usage };
|
|
367
|
+
return { text: result.text, requestId, costNanos, warnings, notices, ...usage };
|
|
266
368
|
} catch (e) {
|
|
267
369
|
await ctx.runMutation(this.component.lib.finishRequest, {
|
|
268
370
|
requestId,
|
|
@@ -281,34 +383,48 @@ export class AIBudget {
|
|
|
281
383
|
*/
|
|
282
384
|
languageModel(
|
|
283
385
|
ctx: RunMutationCtx,
|
|
284
|
-
opts: {
|
|
386
|
+
opts: {
|
|
387
|
+
userId?: string;
|
|
388
|
+
model?: string;
|
|
389
|
+
action?: string;
|
|
390
|
+
/** Extra attribution dimensions to bill/limit (team, customer, env, …). */
|
|
391
|
+
tags?: Tag[];
|
|
392
|
+
} = {}
|
|
285
393
|
): LanguageModel {
|
|
286
394
|
const modelId = opts.model ?? this.defaultModel;
|
|
287
395
|
const component = this.component;
|
|
288
|
-
const
|
|
396
|
+
const fireBudgetEvents = this.fireBudgetEvents.bind(this);
|
|
397
|
+
const fireLimitReached = this.fireLimitReached.bind(this);
|
|
289
398
|
|
|
290
399
|
const begin = async (params: any) => {
|
|
291
400
|
const userId = await resolveUserId(ctx, opts.userId);
|
|
292
401
|
const actionName = await resolveActionName(ctx, opts.action);
|
|
402
|
+
const base = { userId, action: actionName, tags: opts.tags };
|
|
293
403
|
const started = await ctx.runMutation(component.lib.startRequest, {
|
|
294
404
|
userId,
|
|
295
405
|
actionName,
|
|
406
|
+
tags: opts.tags,
|
|
296
407
|
model: modelId,
|
|
297
408
|
messages: simplifyPrompt(params.prompt),
|
|
298
409
|
});
|
|
299
410
|
if (!started.allowed) {
|
|
411
|
+
await fireLimitReached({
|
|
412
|
+
...base,
|
|
413
|
+
messages: [started.reason],
|
|
414
|
+
code: started.code,
|
|
415
|
+
reason: started.reason,
|
|
416
|
+
});
|
|
300
417
|
throw new ConvexError({
|
|
301
418
|
kind: "AIBudgetLimit",
|
|
302
419
|
code: started.code,
|
|
303
420
|
reason: started.reason,
|
|
304
421
|
});
|
|
305
422
|
}
|
|
306
|
-
await
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
423
|
+
await fireBudgetEvents(
|
|
424
|
+
{ ...base, requestId: started.requestId },
|
|
425
|
+
started.warnings,
|
|
426
|
+
started.notices
|
|
427
|
+
);
|
|
312
428
|
return started.requestId;
|
|
313
429
|
};
|
|
314
430
|
const finish = async (
|
|
@@ -318,6 +434,8 @@ export class AIBudget {
|
|
|
318
434
|
error?: string;
|
|
319
435
|
promptTokens?: number;
|
|
320
436
|
completionTokens?: number;
|
|
437
|
+
cachedTokens?: number;
|
|
438
|
+
costNanos?: number;
|
|
321
439
|
latencyMs?: number;
|
|
322
440
|
}
|
|
323
441
|
) => ctx.runMutation(component.lib.finishRequest, { requestId, ...fields });
|
|
@@ -333,6 +451,7 @@ export class AIBudget {
|
|
|
333
451
|
await finish(requestId, {
|
|
334
452
|
responseText: extractText(result),
|
|
335
453
|
...extractUsage(result.usage),
|
|
454
|
+
costNanos: extractGatewayCostNanos(result),
|
|
336
455
|
latencyMs: Date.now() - start,
|
|
337
456
|
});
|
|
338
457
|
return result;
|
|
@@ -409,6 +528,7 @@ export class AIBudget {
|
|
|
409
528
|
messages: args.messages ?? original.messages,
|
|
410
529
|
rerunOf: args.requestId,
|
|
411
530
|
action: original.actionName,
|
|
531
|
+
tags: original.tags,
|
|
412
532
|
});
|
|
413
533
|
}
|
|
414
534
|
|
|
@@ -418,8 +538,16 @@ export class AIBudget {
|
|
|
418
538
|
get requests() {
|
|
419
539
|
const c = this.component;
|
|
420
540
|
return {
|
|
421
|
-
|
|
422
|
-
|
|
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),
|
|
423
551
|
/** Ancestors up to the original, plus direct re-runs. */
|
|
424
552
|
lineage: (ctx: RunQueryCtx, args: { requestId: string }) =>
|
|
425
553
|
ctx.runQuery(c.lib.lineage, { requestId: args.requestId as any }),
|
|
@@ -431,60 +559,97 @@ export class AIBudget {
|
|
|
431
559
|
};
|
|
432
560
|
}
|
|
433
561
|
|
|
434
|
-
/**
|
|
435
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Budgets and controls for an arbitrary attribution dimension — the
|
|
564
|
+
* generalization of `users`/`actions`. Give it any dimension name (team,
|
|
565
|
+
* project, tenant, customer, env, feature, …) and set caps per value:
|
|
566
|
+
*
|
|
567
|
+
* ai.tag("customer").setLimits(ctx, { value: "acme", monthlySpendLimitNanos });
|
|
568
|
+
* ai.tag("customer").history(ctx, { value: "acme", period: "day" });
|
|
569
|
+
*
|
|
570
|
+
* Attribute a call to it by passing `tags` to `chat`/`languageModel`.
|
|
571
|
+
*/
|
|
572
|
+
tag(dimension: string) {
|
|
573
|
+
return this.dimensionApi(dimension, (a: { value: string }) => a.value);
|
|
574
|
+
}
|
|
575
|
+
|
|
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
|
+
) {
|
|
436
582
|
const c = this.component;
|
|
437
583
|
return {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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;
|
|
591
|
+
return ctx.runMutation(c.lib.setBucketLimits, {
|
|
592
|
+
dimension,
|
|
593
|
+
value: key(args),
|
|
594
|
+
...limits,
|
|
595
|
+
});
|
|
596
|
+
},
|
|
597
|
+
/** One-time "approve another $X" bump (daily/monthly reset with the window). */
|
|
598
|
+
bump: (ctx: RunMutationCtx, args: A & BumpArgs) =>
|
|
599
|
+
ctx.runMutation(c.lib.bumpBucket, {
|
|
600
|
+
dimension,
|
|
601
|
+
value: key(args),
|
|
602
|
+
dailyNanos: args.dailyNanos,
|
|
603
|
+
monthlyNanos: args.monthlyNanos,
|
|
604
|
+
lifetimeNanos: args.lifetimeNanos,
|
|
605
|
+
}),
|
|
606
|
+
/** Manually credit (negative) or debit (positive) this bucket. */
|
|
607
|
+
adjust: (
|
|
454
608
|
ctx: RunMutationCtx,
|
|
455
|
-
args: {
|
|
456
|
-
) =>
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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,
|
|
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) }),
|
|
460
639
|
};
|
|
461
640
|
}
|
|
462
641
|
|
|
463
|
-
/** Per-
|
|
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
|
+
|
|
647
|
+
/** Per-action (per-feature) budgets — sugar over the "action" dimension. */
|
|
464
648
|
get actions() {
|
|
465
|
-
|
|
466
|
-
return {
|
|
467
|
-
list: (ctx: RunQueryCtx) => ctx.runQuery(c.lib.listActions, {}),
|
|
468
|
-
setLimits: (
|
|
469
|
-
ctx: RunMutationCtx,
|
|
470
|
-
args: {
|
|
471
|
-
name: string;
|
|
472
|
-
dailySpendLimitNanos?: number;
|
|
473
|
-
lifetimeSpendLimitNanos?: number;
|
|
474
|
-
dailyTokenLimit?: number;
|
|
475
|
-
lifetimeTokenLimit?: number;
|
|
476
|
-
enforcement?: "hard" | "soft";
|
|
477
|
-
disabled?: boolean;
|
|
478
|
-
}
|
|
479
|
-
) => ctx.runMutation(c.lib.setActionLimits, args),
|
|
480
|
-
bump: (
|
|
481
|
-
ctx: RunMutationCtx,
|
|
482
|
-
args: { name: string; dailyNanos?: number; lifetimeNanos?: number }
|
|
483
|
-
) => ctx.runMutation(c.lib.bumpAction, args),
|
|
484
|
-
};
|
|
649
|
+
return this.dimensionApi<{ name: string }>("action", (a) => a.name);
|
|
485
650
|
}
|
|
486
651
|
|
|
487
|
-
/** The deployment-wide budget and retention config. */
|
|
652
|
+
/** The deployment-wide budget, alerts, and retention config. */
|
|
488
653
|
get global() {
|
|
489
654
|
const c = this.component;
|
|
490
655
|
return {
|
|
@@ -503,6 +668,9 @@ export class AIBudget {
|
|
|
503
668
|
ctx: RunMutationCtx,
|
|
504
669
|
args: { dailyNanos?: number; lifetimeNanos?: number }
|
|
505
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),
|
|
506
674
|
/** Request-row retention window in ms (default 1h; 0 disables). */
|
|
507
675
|
setRetention: (ctx: RunMutationCtx, args: { retentionMs: number }) =>
|
|
508
676
|
ctx.runMutation(c.lib.setRetention, args),
|
|
@@ -533,6 +701,8 @@ export class AIBudget {
|
|
|
533
701
|
model: string;
|
|
534
702
|
inputNanosPerMTok: number;
|
|
535
703
|
outputNanosPerMTok: number;
|
|
704
|
+
/** Cache-read rate; defaults to a discount off input if omitted. */
|
|
705
|
+
cachedNanosPerMTok?: number;
|
|
536
706
|
}
|
|
537
707
|
) => ctx.runMutation(c.lib.setPrice, args),
|
|
538
708
|
};
|
|
@@ -24,31 +24,43 @@ import type { FunctionReference } from "convex/server";
|
|
|
24
24
|
export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
25
25
|
{
|
|
26
26
|
lib: {
|
|
27
|
-
|
|
27
|
+
adjustBucket: FunctionReference<
|
|
28
28
|
"mutation",
|
|
29
29
|
"internal",
|
|
30
|
-
{
|
|
30
|
+
{
|
|
31
|
+
deltaNanos: number;
|
|
32
|
+
dimension: string;
|
|
33
|
+
reason?: string;
|
|
34
|
+
tokens?: number;
|
|
35
|
+
value: string;
|
|
36
|
+
},
|
|
31
37
|
null,
|
|
32
38
|
Name
|
|
33
39
|
>;
|
|
34
|
-
|
|
40
|
+
bumpBucket: FunctionReference<
|
|
35
41
|
"mutation",
|
|
36
42
|
"internal",
|
|
37
|
-
{
|
|
43
|
+
{
|
|
44
|
+
dailyNanos?: number;
|
|
45
|
+
dimension: string;
|
|
46
|
+
lifetimeNanos?: number;
|
|
47
|
+
monthlyNanos?: number;
|
|
48
|
+
value: string;
|
|
49
|
+
},
|
|
38
50
|
null,
|
|
39
51
|
Name
|
|
40
52
|
>;
|
|
41
|
-
|
|
53
|
+
bumpGlobal: FunctionReference<
|
|
42
54
|
"mutation",
|
|
43
55
|
"internal",
|
|
44
|
-
{ dailyNanos?: number; lifetimeNanos?: number;
|
|
56
|
+
{ dailyNanos?: number; lifetimeNanos?: number; monthlyNanos?: number },
|
|
45
57
|
null,
|
|
46
58
|
Name
|
|
47
59
|
>;
|
|
48
|
-
|
|
60
|
+
deleteBucket: FunctionReference<
|
|
49
61
|
"mutation",
|
|
50
62
|
"internal",
|
|
51
|
-
{
|
|
63
|
+
{ dimension: string; value: string },
|
|
52
64
|
{ deletedThisBatch: number; done: boolean },
|
|
53
65
|
Name
|
|
54
66
|
>;
|
|
@@ -58,6 +70,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
58
70
|
{
|
|
59
71
|
cachedTokens?: number;
|
|
60
72
|
completionTokens?: number;
|
|
73
|
+
costNanos?: number;
|
|
61
74
|
error?: string;
|
|
62
75
|
latencyMs?: number;
|
|
63
76
|
promptTokens?: number;
|
|
@@ -67,6 +80,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
67
80
|
{ costNanos: number },
|
|
68
81
|
Name
|
|
69
82
|
>;
|
|
83
|
+
getBucket: FunctionReference<
|
|
84
|
+
"query",
|
|
85
|
+
"internal",
|
|
86
|
+
{ dimension: string; value: string },
|
|
87
|
+
any,
|
|
88
|
+
Name
|
|
89
|
+
>;
|
|
70
90
|
getGlobalStatus: FunctionReference<
|
|
71
91
|
"query",
|
|
72
92
|
"internal",
|
|
@@ -101,54 +121,63 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
101
121
|
any,
|
|
102
122
|
Name
|
|
103
123
|
>;
|
|
104
|
-
|
|
124
|
+
listAdjustments: FunctionReference<
|
|
125
|
+
"query",
|
|
126
|
+
"internal",
|
|
127
|
+
{ dimension: string; limit?: number; value: string },
|
|
128
|
+
any,
|
|
129
|
+
Name
|
|
130
|
+
>;
|
|
131
|
+
listBuckets: FunctionReference<
|
|
132
|
+
"query",
|
|
133
|
+
"internal",
|
|
134
|
+
{ dimension?: string },
|
|
135
|
+
any,
|
|
136
|
+
Name
|
|
137
|
+
>;
|
|
105
138
|
listPrices: FunctionReference<"query", "internal", {}, any, Name>;
|
|
106
139
|
listRequests: FunctionReference<
|
|
107
140
|
"query",
|
|
108
141
|
"internal",
|
|
109
|
-
{ limit?: number; userId?: string },
|
|
142
|
+
{ dimension?: string; limit?: number; userId?: string; value?: string },
|
|
110
143
|
any,
|
|
111
144
|
Name
|
|
112
145
|
>;
|
|
113
|
-
|
|
114
|
-
setActionLimits: FunctionReference<
|
|
146
|
+
setAlertDefaults: FunctionReference<
|
|
115
147
|
"mutation",
|
|
116
148
|
"internal",
|
|
117
|
-
{
|
|
118
|
-
dailySpendLimitNanos?: number;
|
|
119
|
-
dailyTokenLimit?: number;
|
|
120
|
-
disabled?: boolean;
|
|
121
|
-
enforcement?: "hard" | "soft";
|
|
122
|
-
lifetimeSpendLimitNanos?: number;
|
|
123
|
-
lifetimeTokenLimit?: number;
|
|
124
|
-
name: string;
|
|
125
|
-
},
|
|
149
|
+
{ warnAtPct?: number },
|
|
126
150
|
null,
|
|
127
151
|
Name
|
|
128
152
|
>;
|
|
129
|
-
|
|
153
|
+
setBucketLimits: FunctionReference<
|
|
130
154
|
"mutation",
|
|
131
155
|
"internal",
|
|
132
156
|
{
|
|
157
|
+
blocked?: boolean;
|
|
133
158
|
dailySpendLimitNanos?: number;
|
|
159
|
+
dailyTokenLimit?: number;
|
|
160
|
+
dimension: string;
|
|
134
161
|
enforcement?: "hard" | "soft";
|
|
135
162
|
lifetimeSpendLimitNanos?: number;
|
|
163
|
+
lifetimeTokenLimit?: number;
|
|
164
|
+
maxConcurrent?: number;
|
|
165
|
+
monthlySpendLimitNanos?: number;
|
|
166
|
+
monthlyTokenLimit?: number;
|
|
167
|
+
requestsPerMinute?: number;
|
|
168
|
+
value: string;
|
|
169
|
+
warnAtPct?: number;
|
|
136
170
|
},
|
|
137
171
|
null,
|
|
138
172
|
Name
|
|
139
173
|
>;
|
|
140
|
-
|
|
174
|
+
setGlobalLimits: FunctionReference<
|
|
141
175
|
"mutation",
|
|
142
176
|
"internal",
|
|
143
177
|
{
|
|
144
|
-
blocked?: boolean;
|
|
145
178
|
dailySpendLimitNanos?: number;
|
|
146
|
-
dailyTokenLimit?: number;
|
|
147
179
|
enforcement?: "hard" | "soft";
|
|
148
180
|
lifetimeSpendLimitNanos?: number;
|
|
149
|
-
lifetimeTokenLimit?: number;
|
|
150
|
-
requestsPerMinute?: number;
|
|
151
|
-
userId: string;
|
|
152
181
|
},
|
|
153
182
|
null,
|
|
154
183
|
Name
|
|
@@ -164,6 +193,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
164
193
|
"mutation",
|
|
165
194
|
"internal",
|
|
166
195
|
{
|
|
196
|
+
cachedNanosPerMTok?: number;
|
|
167
197
|
inputNanosPerMTok: number;
|
|
168
198
|
model: string;
|
|
169
199
|
outputNanosPerMTok: number;
|
|
@@ -186,11 +216,29 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
186
216
|
messages: Array<{ content: string; role: string }>;
|
|
187
217
|
model: string;
|
|
188
218
|
rerunOf?: string;
|
|
219
|
+
tags?: Array<{ dimension: string; value: string }>;
|
|
189
220
|
userId: string;
|
|
190
221
|
},
|
|
191
|
-
| {
|
|
222
|
+
| {
|
|
223
|
+
allowed: true;
|
|
224
|
+
notices: Array<string>;
|
|
225
|
+
requestId: string;
|
|
226
|
+
warnings: Array<string>;
|
|
227
|
+
}
|
|
192
228
|
| { allowed: false; code: string; reason: string },
|
|
193
229
|
Name
|
|
194
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
|
+
>;
|
|
195
243
|
};
|
|
196
244
|
};
|