@convex-dev/ai-budget 0.0.2-alpha.4 → 0.0.2-alpha.6
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 +98 -6
- package/dist/client/dashboard.d.ts +1 -0
- package/dist/client/dashboard.js +215 -0
- package/dist/client/index.d.ts +347 -44
- package/dist/client/index.js +222 -68
- package/dist/component/_generated/component.d.ts +34 -0
- package/dist/component/lib.d.ts +75 -0
- package/dist/component/lib.js +309 -32
- package/dist/component/schema.d.ts +76 -3
- package/dist/component/schema.js +56 -2
- package/package.json +1 -1
- package/src/client/dashboard.ts +215 -0
- package/src/client/index.ts +334 -132
- package/src/component/_generated/component.ts +56 -3
- package/src/component/lib.test.ts +118 -0
- package/src/component/lib.ts +380 -48
- package/src/component/schema.ts +59 -2
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { httpActionGeneric, } from "convex/server";
|
|
1
2
|
import { ConvexError } from "convex/values";
|
|
2
3
|
import { generateText, wrapLanguageModel } from "ai";
|
|
3
4
|
import { convexGateway } from "@convex-dev/ai-sdk-provider";
|
|
5
|
+
import { DASHBOARD_HTML } from "./dashboard";
|
|
4
6
|
// The calling Convex action's name (e.g. "ai:sendMessage"), unless overridden.
|
|
5
7
|
async function resolveActionName(ctx, explicit) {
|
|
6
8
|
if (explicit !== undefined)
|
|
@@ -39,14 +41,30 @@ function extractUsage(usage) {
|
|
|
39
41
|
return {
|
|
40
42
|
promptTokens: toTokenCount(usage?.inputTokens ?? usage?.promptTokens),
|
|
41
43
|
completionTokens: toTokenCount(usage?.outputTokens ?? usage?.completionTokens),
|
|
42
|
-
// cached prompt tokens
|
|
43
|
-
// `
|
|
44
|
-
|
|
44
|
+
// cached prompt tokens. The Convex gateway reports these at
|
|
45
|
+
// `usage.inputTokenDetails.cacheReadTokens`; the other paths cover AI SDK v5
|
|
46
|
+
// (`cachedInputTokens`) and raw OpenAI-compatible shapes.
|
|
47
|
+
cachedTokens: toTokenCount(usage?.inputTokenDetails?.cacheReadTokens ??
|
|
48
|
+
usage?.cachedInputTokens ??
|
|
45
49
|
usage?.promptTokensDetails?.cachedTokens ??
|
|
46
50
|
usage?.prompt_tokens_details?.cached_tokens ??
|
|
47
51
|
usage?.cached_tokens),
|
|
48
52
|
};
|
|
49
53
|
}
|
|
54
|
+
// The gateway doesn't report a dollar cost today, but if a future response ever
|
|
55
|
+
// carries an unambiguous nanodollar cost we pass it straight through as
|
|
56
|
+
// authoritative (finishRequest prefers it over the token-based estimate). Only
|
|
57
|
+
// an explicitly nano-denominated field is trusted — a bare `cost` could be in
|
|
58
|
+
// dollars and silently mis-bill by 1e9×.
|
|
59
|
+
function extractGatewayCostNanos(result) {
|
|
60
|
+
const meta = result?.providerMetadata?.convexGateway;
|
|
61
|
+
const candidates = [meta?.costNanos, result?.usage?.costNanos];
|
|
62
|
+
for (const c of candidates) {
|
|
63
|
+
if (typeof c === "number" && Number.isFinite(c) && c >= 0)
|
|
64
|
+
return c;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
50
68
|
// Flatten an AI SDK prompt (roles + content parts) into simple storable messages.
|
|
51
69
|
function simplifyPrompt(prompt) {
|
|
52
70
|
if (!Array.isArray(prompt))
|
|
@@ -78,24 +96,46 @@ function extractText(result) {
|
|
|
78
96
|
}
|
|
79
97
|
return "";
|
|
80
98
|
}
|
|
81
|
-
// ---------- client ----------
|
|
82
99
|
export class AIBudget {
|
|
83
100
|
component;
|
|
84
101
|
defaultModel;
|
|
85
102
|
onSoftLimit;
|
|
103
|
+
onThreshold;
|
|
104
|
+
onLimitReached;
|
|
86
105
|
constructor(component, options) {
|
|
87
106
|
this.component = component;
|
|
88
107
|
this.defaultModel = options?.defaultModel ?? "openai/gpt-4o-mini";
|
|
89
108
|
this.onSoftLimit = options?.onSoftLimit;
|
|
109
|
+
this.onThreshold = options?.onThreshold;
|
|
110
|
+
this.onLimitReached = options?.onLimitReached;
|
|
111
|
+
}
|
|
112
|
+
// Fire the soft-limit + threshold callbacks from a startRequest result.
|
|
113
|
+
async fireBudgetEvents(base, warnings, notices) {
|
|
114
|
+
if (warnings.length > 0 && this.onSoftLimit) {
|
|
115
|
+
try {
|
|
116
|
+
await this.onSoftLimit({ ...base, messages: warnings, warnings });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* never let a callback error break a request */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (notices.length > 0 && this.onThreshold) {
|
|
123
|
+
try {
|
|
124
|
+
await this.onThreshold({ ...base, messages: notices });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
/* swallow */
|
|
128
|
+
}
|
|
129
|
+
}
|
|
90
130
|
}
|
|
91
|
-
async
|
|
92
|
-
if (
|
|
131
|
+
async fireLimitReached(info) {
|
|
132
|
+
if (!this.onLimitReached)
|
|
93
133
|
return;
|
|
94
134
|
try {
|
|
95
|
-
await this.
|
|
135
|
+
await this.onLimitReached(info);
|
|
96
136
|
}
|
|
97
137
|
catch {
|
|
98
|
-
|
|
138
|
+
/* swallow */
|
|
99
139
|
}
|
|
100
140
|
}
|
|
101
141
|
/**
|
|
@@ -116,6 +156,14 @@ export class AIBudget {
|
|
|
116
156
|
rerunOf: args.rerunOf,
|
|
117
157
|
});
|
|
118
158
|
if (!started.allowed) {
|
|
159
|
+
await this.fireLimitReached({
|
|
160
|
+
userId,
|
|
161
|
+
action: actionName,
|
|
162
|
+
tags: args.tags,
|
|
163
|
+
messages: [started.reason],
|
|
164
|
+
code: started.code,
|
|
165
|
+
reason: started.reason,
|
|
166
|
+
});
|
|
119
167
|
throw new ConvexError({
|
|
120
168
|
kind: "AIBudgetLimit",
|
|
121
169
|
code: started.code,
|
|
@@ -124,13 +172,8 @@ export class AIBudget {
|
|
|
124
172
|
}
|
|
125
173
|
const requestId = started.requestId;
|
|
126
174
|
const warnings = started.warnings;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
action: actionName,
|
|
130
|
-
tags: args.tags,
|
|
131
|
-
requestId,
|
|
132
|
-
warnings,
|
|
133
|
-
});
|
|
175
|
+
const notices = started.notices;
|
|
176
|
+
await this.fireBudgetEvents({ userId, action: actionName, tags: args.tags, requestId }, warnings, notices);
|
|
134
177
|
const start = Date.now();
|
|
135
178
|
try {
|
|
136
179
|
// The full chain (incl. system) is stored on the request for audit/replay,
|
|
@@ -150,9 +193,10 @@ export class AIBudget {
|
|
|
150
193
|
requestId,
|
|
151
194
|
responseText: result.text,
|
|
152
195
|
...usage,
|
|
196
|
+
costNanos: extractGatewayCostNanos(result),
|
|
153
197
|
latencyMs: Date.now() - start,
|
|
154
198
|
});
|
|
155
|
-
return { text: result.text, requestId, costNanos, warnings, ...usage };
|
|
199
|
+
return { text: result.text, requestId, costNanos, warnings, notices, ...usage };
|
|
156
200
|
}
|
|
157
201
|
catch (e) {
|
|
158
202
|
await ctx.runMutation(this.component.lib.finishRequest, {
|
|
@@ -172,10 +216,12 @@ export class AIBudget {
|
|
|
172
216
|
languageModel(ctx, opts = {}) {
|
|
173
217
|
const modelId = opts.model ?? this.defaultModel;
|
|
174
218
|
const component = this.component;
|
|
175
|
-
const
|
|
219
|
+
const fireBudgetEvents = this.fireBudgetEvents.bind(this);
|
|
220
|
+
const fireLimitReached = this.fireLimitReached.bind(this);
|
|
176
221
|
const begin = async (params) => {
|
|
177
222
|
const userId = await resolveUserId(ctx, opts.userId);
|
|
178
223
|
const actionName = await resolveActionName(ctx, opts.action);
|
|
224
|
+
const base = { userId, action: actionName, tags: opts.tags };
|
|
179
225
|
const started = await ctx.runMutation(component.lib.startRequest, {
|
|
180
226
|
userId,
|
|
181
227
|
actionName,
|
|
@@ -184,19 +230,19 @@ export class AIBudget {
|
|
|
184
230
|
messages: simplifyPrompt(params.prompt),
|
|
185
231
|
});
|
|
186
232
|
if (!started.allowed) {
|
|
233
|
+
await fireLimitReached({
|
|
234
|
+
...base,
|
|
235
|
+
messages: [started.reason],
|
|
236
|
+
code: started.code,
|
|
237
|
+
reason: started.reason,
|
|
238
|
+
});
|
|
187
239
|
throw new ConvexError({
|
|
188
240
|
kind: "AIBudgetLimit",
|
|
189
241
|
code: started.code,
|
|
190
242
|
reason: started.reason,
|
|
191
243
|
});
|
|
192
244
|
}
|
|
193
|
-
await
|
|
194
|
-
userId,
|
|
195
|
-
action: actionName,
|
|
196
|
-
tags: opts.tags,
|
|
197
|
-
requestId: started.requestId,
|
|
198
|
-
warnings: started.warnings,
|
|
199
|
-
});
|
|
245
|
+
await fireBudgetEvents({ ...base, requestId: started.requestId }, started.warnings, started.notices);
|
|
200
246
|
return started.requestId;
|
|
201
247
|
};
|
|
202
248
|
const finish = async (requestId, fields) => ctx.runMutation(component.lib.finishRequest, { requestId, ...fields });
|
|
@@ -211,6 +257,7 @@ export class AIBudget {
|
|
|
211
257
|
await finish(requestId, {
|
|
212
258
|
responseText: extractText(result),
|
|
213
259
|
...extractUsage(result.usage),
|
|
260
|
+
costNanos: extractGatewayCostNanos(result),
|
|
214
261
|
latencyMs: Date.now() - start,
|
|
215
262
|
});
|
|
216
263
|
return result;
|
|
@@ -295,6 +342,7 @@ export class AIBudget {
|
|
|
295
342
|
get requests() {
|
|
296
343
|
const c = this.component;
|
|
297
344
|
return {
|
|
345
|
+
/** Filter by userId, or by any {dimension, value} (incl. custom tags). */
|
|
298
346
|
list: (ctx, args = {}) => ctx.runQuery(c.lib.listRequests, args),
|
|
299
347
|
/** Ancestors up to the original, plus direct re-runs. */
|
|
300
348
|
lineage: (ctx, args) => ctx.runQuery(c.lib.lineage, { requestId: args.requestId }),
|
|
@@ -307,74 +355,73 @@ export class AIBudget {
|
|
|
307
355
|
* generalization of `users`/`actions`. Give it any dimension name (team,
|
|
308
356
|
* project, tenant, customer, env, feature, …) and set caps per value:
|
|
309
357
|
*
|
|
310
|
-
* ai.tag("customer").setLimits(ctx, { value: "acme",
|
|
311
|
-
* ai.tag("customer").
|
|
358
|
+
* ai.tag("customer").setLimits(ctx, { value: "acme", monthlySpendLimitNanos });
|
|
359
|
+
* ai.tag("customer").history(ctx, { value: "acme", period: "day" });
|
|
312
360
|
*
|
|
313
361
|
* Attribute a call to it by passing `tags` to `chat`/`languageModel`.
|
|
314
362
|
*/
|
|
315
363
|
tag(dimension) {
|
|
364
|
+
return this.dimensionApi(dimension, (a) => a.value);
|
|
365
|
+
}
|
|
366
|
+
// Shared implementation behind tag()/users/actions. `key` maps the namespace's
|
|
367
|
+
// id field (value/userId/name) to the bucket value.
|
|
368
|
+
dimensionApi(dimension, key) {
|
|
316
369
|
const c = this.component;
|
|
317
370
|
return {
|
|
318
371
|
/** All buckets in this dimension. */
|
|
319
372
|
list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension }),
|
|
320
373
|
/** One bucket's limits + spend (null if it has none yet). */
|
|
321
|
-
get: (ctx, args) => ctx.runQuery(c.lib.getBucket, { dimension, value: args
|
|
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. */
|
|
330
|
-
get users() {
|
|
331
|
-
const c = this.component;
|
|
332
|
-
return {
|
|
333
|
-
list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension: "user" }),
|
|
374
|
+
get: (ctx, args) => ctx.runQuery(c.lib.getBucket, { dimension, value: key(args) }),
|
|
334
375
|
setLimits: (ctx, args) => {
|
|
335
|
-
const { userId, ...limits } = args;
|
|
376
|
+
const { value, userId, name, ...limits } = args;
|
|
336
377
|
return ctx.runMutation(c.lib.setBucketLimits, {
|
|
337
|
-
dimension
|
|
338
|
-
value:
|
|
378
|
+
dimension,
|
|
379
|
+
value: key(args),
|
|
339
380
|
...limits,
|
|
340
381
|
});
|
|
341
382
|
},
|
|
342
|
-
/** One-time "approve another $X" bump (daily
|
|
383
|
+
/** One-time "approve another $X" bump (daily/monthly reset with the window). */
|
|
343
384
|
bump: (ctx, args) => ctx.runMutation(c.lib.bumpBucket, {
|
|
344
|
-
dimension
|
|
345
|
-
value: args
|
|
385
|
+
dimension,
|
|
386
|
+
value: key(args),
|
|
346
387
|
dailyNanos: args.dailyNanos,
|
|
388
|
+
monthlyNanos: args.monthlyNanos,
|
|
347
389
|
lifetimeNanos: args.lifetimeNanos,
|
|
348
390
|
}),
|
|
349
|
-
/**
|
|
350
|
-
|
|
351
|
-
dimension
|
|
352
|
-
value: args
|
|
391
|
+
/** Manually credit (negative) or debit (positive) this bucket. */
|
|
392
|
+
adjust: (ctx, args) => ctx.runMutation(c.lib.adjustBucket, {
|
|
393
|
+
dimension,
|
|
394
|
+
value: key(args),
|
|
395
|
+
deltaNanos: args.deltaNanos,
|
|
396
|
+
tokens: args.tokens,
|
|
397
|
+
reason: args.reason,
|
|
398
|
+
}),
|
|
399
|
+
/** Durable spend history for this bucket (per day or per month). */
|
|
400
|
+
history: (ctx, args) => ctx.runQuery(c.lib.usageHistory, {
|
|
401
|
+
dimension,
|
|
402
|
+
value: key(args),
|
|
403
|
+
period: args.period ?? "day",
|
|
404
|
+
limit: args.limit,
|
|
353
405
|
}),
|
|
406
|
+
/** Manual-adjustment audit log for this bucket. */
|
|
407
|
+
adjustments: (ctx, args) => ctx.runQuery(c.lib.listAdjustments, {
|
|
408
|
+
dimension,
|
|
409
|
+
value: key(args),
|
|
410
|
+
limit: args.limit,
|
|
411
|
+
}),
|
|
412
|
+
/** Delete the bucket (for "user", also its request rows). */
|
|
413
|
+
delete: (ctx, args) => ctx.runMutation(c.lib.deleteBucket, { dimension, value: key(args) }),
|
|
354
414
|
};
|
|
355
415
|
}
|
|
416
|
+
/** Per-user budgets and controls — sugar over the "user" dimension. */
|
|
417
|
+
get users() {
|
|
418
|
+
return this.dimensionApi("user", (a) => a.userId);
|
|
419
|
+
}
|
|
356
420
|
/** Per-action (per-feature) budgets — sugar over the "action" dimension. */
|
|
357
421
|
get actions() {
|
|
358
|
-
|
|
359
|
-
return {
|
|
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
|
-
}),
|
|
375
|
-
};
|
|
422
|
+
return this.dimensionApi("action", (a) => a.name);
|
|
376
423
|
}
|
|
377
|
-
/** The deployment-wide budget and retention config. */
|
|
424
|
+
/** The deployment-wide budget, alerts, and retention config. */
|
|
378
425
|
get global() {
|
|
379
426
|
const c = this.component;
|
|
380
427
|
return {
|
|
@@ -383,6 +430,8 @@ export class AIBudget {
|
|
|
383
430
|
/** A killswitch spend cap across all users/actions (enforced approximately). */
|
|
384
431
|
setLimits: (ctx, args) => ctx.runMutation(c.lib.setGlobalLimits, args),
|
|
385
432
|
bump: (ctx, args) => ctx.runMutation(c.lib.bumpGlobal, args),
|
|
433
|
+
/** Default approaching-limit alert threshold (fraction of a cap, e.g. 0.8). */
|
|
434
|
+
setAlertDefaults: (ctx, args) => ctx.runMutation(c.lib.setAlertDefaults, args),
|
|
386
435
|
/** Request-row retention window in ms (default 1h; 0 disables). */
|
|
387
436
|
setRetention: (ctx, args) => ctx.runMutation(c.lib.setRetention, args),
|
|
388
437
|
};
|
|
@@ -404,6 +453,111 @@ export class AIBudget {
|
|
|
404
453
|
set: (ctx, args) => ctx.runMutation(c.lib.setPrice, args),
|
|
405
454
|
};
|
|
406
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Mount the built-in admin dashboard on your app's HTTP router with one call.
|
|
458
|
+
* Serves a self-contained HTML dashboard (buckets, requests, usage history,
|
|
459
|
+
* settings) plus a small JSON API, all backed by the component — no extra
|
|
460
|
+
* queries to write.
|
|
461
|
+
*
|
|
462
|
+
* // convex/http.ts
|
|
463
|
+
* import { httpRouter } from "convex/server";
|
|
464
|
+
* const http = httpRouter();
|
|
465
|
+
* ai.registerRoutes(http, { authorize: async (ctx) =>
|
|
466
|
+
* (await ctx.auth.getUserIdentity())?.role === "admin" });
|
|
467
|
+
* export default http;
|
|
468
|
+
*
|
|
469
|
+
* It then lives at `https://<deployment>.convex.site/aibudget`.
|
|
470
|
+
*
|
|
471
|
+
* SECURITY: the endpoint is public on the internet. You MUST gate it — either
|
|
472
|
+
* pass `authorize` (recommended: check the caller is a deployment admin) or
|
|
473
|
+
* set the `AI_BUDGET_DASHBOARD_TOKEN` env var (a bearer token / `?token=`).
|
|
474
|
+
* With neither, every route returns 401.
|
|
475
|
+
*/
|
|
476
|
+
registerRoutes(http, opts = {}) {
|
|
477
|
+
const prefix = (opts.path ?? "/aibudget").replace(/\/+$/, "");
|
|
478
|
+
const c = this.component.lib;
|
|
479
|
+
const authorize = opts.authorize;
|
|
480
|
+
const guard = async (ctx, request) => {
|
|
481
|
+
if (authorize)
|
|
482
|
+
return { ok: await authorize(ctx, request), token: "" };
|
|
483
|
+
const token = globalThis.process?.env?.AI_BUDGET_DASHBOARD_TOKEN;
|
|
484
|
+
if (!token)
|
|
485
|
+
return { ok: false, token: "" };
|
|
486
|
+
const url = new URL(request.url);
|
|
487
|
+
const bearer = (request.headers.get("authorization") ?? "").replace(/^Bearer\s+/i, "");
|
|
488
|
+
const provided = bearer || url.searchParams.get("token") || "";
|
|
489
|
+
return { ok: provided === token, token };
|
|
490
|
+
};
|
|
491
|
+
const json = (data, status = 200) => new Response(JSON.stringify(data ?? null), {
|
|
492
|
+
status,
|
|
493
|
+
headers: { "content-type": "application/json" },
|
|
494
|
+
});
|
|
495
|
+
const handle = async (ctx, request) => {
|
|
496
|
+
const url = new URL(request.url);
|
|
497
|
+
const sub = url.pathname.slice(prefix.length) || "/";
|
|
498
|
+
const { ok, token } = await guard(ctx, request);
|
|
499
|
+
if (!ok) {
|
|
500
|
+
return new Response("Unauthorized. Pass `authorize` to registerRoutes or set AI_BUDGET_DASHBOARD_TOKEN.", { status: 401 });
|
|
501
|
+
}
|
|
502
|
+
if (sub.startsWith("/api/")) {
|
|
503
|
+
const route = request.method + " " + sub.slice(4); // strip "/api"
|
|
504
|
+
const p = {};
|
|
505
|
+
url.searchParams.forEach((v, k) => {
|
|
506
|
+
p[k] = v;
|
|
507
|
+
});
|
|
508
|
+
const body = request.method === "POST"
|
|
509
|
+
? await request.json().catch(() => ({}))
|
|
510
|
+
: {};
|
|
511
|
+
switch (route) {
|
|
512
|
+
case "GET /buckets":
|
|
513
|
+
return json(await ctx.runQuery(c.listBuckets, { dimension: p.dimension || undefined }));
|
|
514
|
+
case "GET /requests":
|
|
515
|
+
return json(await ctx.runQuery(c.listRequests, {
|
|
516
|
+
userId: p.userId || undefined,
|
|
517
|
+
dimension: p.dimension || undefined,
|
|
518
|
+
value: p.value || undefined,
|
|
519
|
+
limit: 100,
|
|
520
|
+
}));
|
|
521
|
+
case "GET /usage":
|
|
522
|
+
return json(await ctx.runQuery(c.usageHistory, {
|
|
523
|
+
dimension: p.dimension,
|
|
524
|
+
value: p.value,
|
|
525
|
+
period: p.period === "month" ? "month" : "day",
|
|
526
|
+
}));
|
|
527
|
+
case "GET /global":
|
|
528
|
+
return json(await ctx.runQuery(c.getGlobalStatus, {}));
|
|
529
|
+
case "GET /prices":
|
|
530
|
+
return json(await ctx.runQuery(c.listPrices, {}));
|
|
531
|
+
case "POST /setLimits":
|
|
532
|
+
return json(await ctx.runMutation(c.setBucketLimits, body));
|
|
533
|
+
case "POST /bump":
|
|
534
|
+
return json(await ctx.runMutation(c.bumpBucket, body));
|
|
535
|
+
case "POST /adjust":
|
|
536
|
+
return json(await ctx.runMutation(c.adjustBucket, body));
|
|
537
|
+
case "POST /delete":
|
|
538
|
+
return json(await ctx.runMutation(c.deleteBucket, body));
|
|
539
|
+
case "POST /global/setLimits":
|
|
540
|
+
return json(await ctx.runMutation(c.setGlobalLimits, body));
|
|
541
|
+
case "POST /global/setAlertDefaults":
|
|
542
|
+
return json(await ctx.runMutation(c.setAlertDefaults, body));
|
|
543
|
+
case "POST /global/setRetention":
|
|
544
|
+
return json(await ctx.runMutation(c.setRetention, body));
|
|
545
|
+
case "POST /setPrice":
|
|
546
|
+
return json(await ctx.runMutation(c.setPrice, body));
|
|
547
|
+
default:
|
|
548
|
+
return json({ error: "not found" }, 404);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const html = DASHBOARD_HTML.replace(/__API_BASE__/g, `${prefix}/api`).replace(/__TOKEN__/g, token);
|
|
552
|
+
return new Response(html, {
|
|
553
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
const handler = httpActionGeneric(handle);
|
|
557
|
+
http.route({ path: prefix, method: "GET", handler });
|
|
558
|
+
http.route({ pathPrefix: `${prefix}/`, method: "GET", handler });
|
|
559
|
+
http.route({ pathPrefix: `${prefix}/`, method: "POST", handler });
|
|
560
|
+
}
|
|
407
561
|
}
|
|
408
562
|
/** @deprecated Renamed to `AIBudget`. */
|
|
409
563
|
export const WorryFreeAI = AIBudget;
|
|
@@ -20,15 +20,24 @@ import type { FunctionReference } from "convex/server";
|
|
|
20
20
|
*/
|
|
21
21
|
export type ComponentApi<Name extends string | undefined = string | undefined> = {
|
|
22
22
|
lib: {
|
|
23
|
+
adjustBucket: FunctionReference<"mutation", "internal", {
|
|
24
|
+
deltaNanos: number;
|
|
25
|
+
dimension: string;
|
|
26
|
+
reason?: string;
|
|
27
|
+
tokens?: number;
|
|
28
|
+
value: string;
|
|
29
|
+
}, null, Name>;
|
|
23
30
|
bumpBucket: FunctionReference<"mutation", "internal", {
|
|
24
31
|
dailyNanos?: number;
|
|
25
32
|
dimension: string;
|
|
26
33
|
lifetimeNanos?: number;
|
|
34
|
+
monthlyNanos?: number;
|
|
27
35
|
value: string;
|
|
28
36
|
}, null, Name>;
|
|
29
37
|
bumpGlobal: FunctionReference<"mutation", "internal", {
|
|
30
38
|
dailyNanos?: number;
|
|
31
39
|
lifetimeNanos?: number;
|
|
40
|
+
monthlyNanos?: number;
|
|
32
41
|
}, null, Name>;
|
|
33
42
|
deleteBucket: FunctionReference<"mutation", "internal", {
|
|
34
43
|
dimension: string;
|
|
@@ -40,6 +49,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
40
49
|
finishRequest: FunctionReference<"mutation", "internal", {
|
|
41
50
|
cachedTokens?: number;
|
|
42
51
|
completionTokens?: number;
|
|
52
|
+
costNanos?: number;
|
|
43
53
|
error?: string;
|
|
44
54
|
latencyMs?: number;
|
|
45
55
|
promptTokens?: number;
|
|
@@ -54,8 +64,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
54
64
|
}, any, Name>;
|
|
55
65
|
getGlobalStatus: FunctionReference<"query", "internal", {}, {
|
|
56
66
|
dailySpendLimitNanos: number | null;
|
|
67
|
+
defaultWarnAtPct: number | null;
|
|
57
68
|
enforcement: "hard" | "soft";
|
|
58
69
|
lifetimeSpendLimitNanos: number | null;
|
|
70
|
+
retentionMs: number | null;
|
|
59
71
|
spentTodayNanos: number;
|
|
60
72
|
spentTotalNanos: number;
|
|
61
73
|
}, Name>;
|
|
@@ -69,14 +81,24 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
69
81
|
lineage: FunctionReference<"query", "internal", {
|
|
70
82
|
requestId: string;
|
|
71
83
|
}, any, Name>;
|
|
84
|
+
listAdjustments: FunctionReference<"query", "internal", {
|
|
85
|
+
dimension: string;
|
|
86
|
+
limit?: number;
|
|
87
|
+
value: string;
|
|
88
|
+
}, any, Name>;
|
|
72
89
|
listBuckets: FunctionReference<"query", "internal", {
|
|
73
90
|
dimension?: string;
|
|
74
91
|
}, any, Name>;
|
|
75
92
|
listPrices: FunctionReference<"query", "internal", {}, any, Name>;
|
|
76
93
|
listRequests: FunctionReference<"query", "internal", {
|
|
94
|
+
dimension?: string;
|
|
77
95
|
limit?: number;
|
|
78
96
|
userId?: string;
|
|
97
|
+
value?: string;
|
|
79
98
|
}, any, Name>;
|
|
99
|
+
setAlertDefaults: FunctionReference<"mutation", "internal", {
|
|
100
|
+
warnAtPct?: number;
|
|
101
|
+
}, null, Name>;
|
|
80
102
|
setBucketLimits: FunctionReference<"mutation", "internal", {
|
|
81
103
|
blocked?: boolean;
|
|
82
104
|
dailySpendLimitNanos?: number;
|
|
@@ -85,8 +107,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
85
107
|
enforcement?: "hard" | "soft";
|
|
86
108
|
lifetimeSpendLimitNanos?: number;
|
|
87
109
|
lifetimeTokenLimit?: number;
|
|
110
|
+
maxConcurrent?: number;
|
|
111
|
+
monthlySpendLimitNanos?: number;
|
|
112
|
+
monthlyTokenLimit?: number;
|
|
88
113
|
requestsPerMinute?: number;
|
|
89
114
|
value: string;
|
|
115
|
+
warnAtPct?: number;
|
|
90
116
|
}, null, Name>;
|
|
91
117
|
setGlobalLimits: FunctionReference<"mutation", "internal", {
|
|
92
118
|
dailySpendLimitNanos?: number;
|
|
@@ -98,6 +124,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
98
124
|
models: Array<string>;
|
|
99
125
|
}, null, Name>;
|
|
100
126
|
setPrice: FunctionReference<"mutation", "internal", {
|
|
127
|
+
cachedNanosPerMTok?: number;
|
|
101
128
|
inputNanosPerMTok: number;
|
|
102
129
|
model: string;
|
|
103
130
|
outputNanosPerMTok: number;
|
|
@@ -120,6 +147,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
120
147
|
userId: string;
|
|
121
148
|
}, {
|
|
122
149
|
allowed: true;
|
|
150
|
+
notices: Array<string>;
|
|
123
151
|
requestId: string;
|
|
124
152
|
warnings: Array<string>;
|
|
125
153
|
} | {
|
|
@@ -127,5 +155,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
127
155
|
code: string;
|
|
128
156
|
reason: string;
|
|
129
157
|
}, Name>;
|
|
158
|
+
usageHistory: FunctionReference<"query", "internal", {
|
|
159
|
+
dimension: string;
|
|
160
|
+
limit?: number;
|
|
161
|
+
period: "day" | "month";
|
|
162
|
+
value: string;
|
|
163
|
+
}, any, Name>;
|
|
130
164
|
};
|
|
131
165
|
};
|