@convex-dev/ai-budget 0.0.2-alpha.0 → 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 +5 -5
- 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
|
@@ -5,8 +5,15 @@ export declare const vMessage: import("convex/values").VObject<{
|
|
|
5
5
|
role: import("convex/values").VString<string, "required">;
|
|
6
6
|
content: import("convex/values").VString<string, "required">;
|
|
7
7
|
}, "required", "role" | "content">;
|
|
8
|
+
export declare const vTag: import("convex/values").VObject<{
|
|
9
|
+
dimension: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}, {
|
|
12
|
+
dimension: import("convex/values").VString<string, "required">;
|
|
13
|
+
value: import("convex/values").VString<string, "required">;
|
|
14
|
+
}, "required", "dimension" | "value">;
|
|
8
15
|
declare const _default: import("convex/server").SchemaDefinition<{
|
|
9
|
-
|
|
16
|
+
buckets: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
10
17
|
requestsPerMinute?: number | undefined;
|
|
11
18
|
dailySpendLimitNanos?: number | undefined;
|
|
12
19
|
lifetimeSpendLimitNanos?: number | undefined;
|
|
@@ -23,14 +30,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
23
30
|
reservedTodayTokens?: number | undefined;
|
|
24
31
|
reservedTotalTokens?: number | undefined;
|
|
25
32
|
pendingCount?: number | undefined;
|
|
26
|
-
|
|
33
|
+
dimension: string;
|
|
34
|
+
value: string;
|
|
27
35
|
totalSpendNanos: number;
|
|
28
36
|
totalRequests: number;
|
|
29
37
|
totalTokens: number;
|
|
30
38
|
dayStamp: string;
|
|
31
39
|
spendTodayNanos: number;
|
|
32
40
|
}, {
|
|
33
|
-
|
|
41
|
+
dimension: import("convex/values").VString<string, "required">;
|
|
42
|
+
value: import("convex/values").VString<string, "required">;
|
|
34
43
|
requestsPerMinute: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
35
44
|
dailySpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
36
45
|
lifetimeSpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
@@ -52,58 +61,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
52
61
|
reservedTodayTokens: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
53
62
|
reservedTotalTokens: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
54
63
|
pendingCount: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
55
|
-
}, "required", "
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
actions: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
59
|
-
dailySpendLimitNanos?: number | undefined;
|
|
60
|
-
lifetimeSpendLimitNanos?: number | undefined;
|
|
61
|
-
dailyTokenLimit?: number | undefined;
|
|
62
|
-
lifetimeTokenLimit?: number | undefined;
|
|
63
|
-
enforcement?: "hard" | "soft" | undefined;
|
|
64
|
-
dailyBumpNanos?: number | undefined;
|
|
65
|
-
lifetimeBumpNanos?: number | undefined;
|
|
66
|
-
bumpDayStamp?: string | undefined;
|
|
67
|
-
tokensToday?: number | undefined;
|
|
68
|
-
reservedTodayNanos?: number | undefined;
|
|
69
|
-
reservedTotalNanos?: number | undefined;
|
|
70
|
-
reservedTodayTokens?: number | undefined;
|
|
71
|
-
reservedTotalTokens?: number | undefined;
|
|
72
|
-
pendingCount?: number | undefined;
|
|
73
|
-
disabled?: boolean | undefined;
|
|
74
|
-
totalSpendNanos: number;
|
|
75
|
-
totalRequests: number;
|
|
76
|
-
totalTokens: number;
|
|
77
|
-
dayStamp: string;
|
|
78
|
-
spendTodayNanos: number;
|
|
79
|
-
name: string;
|
|
80
|
-
}, {
|
|
81
|
-
name: import("convex/values").VString<string, "required">;
|
|
82
|
-
dailySpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
83
|
-
lifetimeSpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
84
|
-
dailyTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
85
|
-
lifetimeTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
86
|
-
disabled: import("convex/values").VBoolean<boolean | undefined, "optional">;
|
|
87
|
-
enforcement: import("convex/values").VUnion<"hard" | "soft" | undefined, [import("convex/values").VLiteral<"hard", "required">, import("convex/values").VLiteral<"soft", "required">], "optional", never>;
|
|
88
|
-
dailyBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
89
|
-
lifetimeBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
90
|
-
bumpDayStamp: import("convex/values").VString<string | undefined, "optional">;
|
|
91
|
-
totalSpendNanos: import("convex/values").VFloat64<number, "required">;
|
|
92
|
-
totalRequests: import("convex/values").VFloat64<number, "required">;
|
|
93
|
-
totalTokens: import("convex/values").VFloat64<number, "required">;
|
|
94
|
-
dayStamp: import("convex/values").VString<string, "required">;
|
|
95
|
-
spendTodayNanos: import("convex/values").VFloat64<number, "required">;
|
|
96
|
-
tokensToday: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
97
|
-
reservedTodayNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
98
|
-
reservedTotalNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
99
|
-
reservedTodayTokens: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
100
|
-
reservedTotalTokens: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
101
|
-
pendingCount: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
102
|
-
}, "required", "dailySpendLimitNanos" | "lifetimeSpendLimitNanos" | "dailyTokenLimit" | "lifetimeTokenLimit" | "enforcement" | "dailyBumpNanos" | "lifetimeBumpNanos" | "bumpDayStamp" | "totalSpendNanos" | "totalRequests" | "totalTokens" | "dayStamp" | "spendTodayNanos" | "tokensToday" | "reservedTodayNanos" | "reservedTotalNanos" | "reservedTodayTokens" | "reservedTotalTokens" | "pendingCount" | "name" | "disabled">, {
|
|
103
|
-
name: ["name", "_creationTime"];
|
|
64
|
+
}, "required", "dimension" | "value" | "requestsPerMinute" | "dailySpendLimitNanos" | "lifetimeSpendLimitNanos" | "dailyTokenLimit" | "lifetimeTokenLimit" | "blocked" | "enforcement" | "dailyBumpNanos" | "lifetimeBumpNanos" | "bumpDayStamp" | "totalSpendNanos" | "totalRequests" | "totalTokens" | "dayStamp" | "spendTodayNanos" | "tokensToday" | "reservedTodayNanos" | "reservedTotalNanos" | "reservedTodayTokens" | "reservedTotalTokens" | "pendingCount">, {
|
|
65
|
+
dim_value: ["dimension", "value", "_creationTime"];
|
|
66
|
+
dimension: ["dimension", "_creationTime"];
|
|
104
67
|
}, {}, {}>;
|
|
105
68
|
requests: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
106
69
|
actionName?: string | undefined;
|
|
70
|
+
tags?: {
|
|
71
|
+
dimension: string;
|
|
72
|
+
value: string;
|
|
73
|
+
}[] | undefined;
|
|
107
74
|
estimatedNanos?: number | undefined;
|
|
108
75
|
estimatedTokens?: number | undefined;
|
|
109
76
|
unpricedModel?: boolean | undefined;
|
|
@@ -127,6 +94,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
127
94
|
}, {
|
|
128
95
|
userId: import("convex/values").VString<string, "required">;
|
|
129
96
|
actionName: import("convex/values").VString<string | undefined, "optional">;
|
|
97
|
+
tags: import("convex/values").VArray<{
|
|
98
|
+
dimension: string;
|
|
99
|
+
value: string;
|
|
100
|
+
}[] | undefined, import("convex/values").VObject<{
|
|
101
|
+
dimension: string;
|
|
102
|
+
value: string;
|
|
103
|
+
}, {
|
|
104
|
+
dimension: import("convex/values").VString<string, "required">;
|
|
105
|
+
value: import("convex/values").VString<string, "required">;
|
|
106
|
+
}, "required", "dimension" | "value">, "optional">;
|
|
130
107
|
model: import("convex/values").VString<string, "required">;
|
|
131
108
|
estimatedNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
132
109
|
estimatedTokens: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
@@ -152,7 +129,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
152
129
|
costNanos: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
153
130
|
latencyMs: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
154
131
|
rerunOf: import("convex/values").VId<import("convex/values").GenericId<"requests"> | undefined, "optional">;
|
|
155
|
-
}, "required", "userId" | "actionName" | "model" | "estimatedNanos" | "estimatedTokens" | "unpricedModel" | "overBudget" | "settled" | "messages" | "status" | "error" | "responseText" | "promptTokens" | "completionTokens" | "cachedTokens" | "costNanos" | "latencyMs" | "rerunOf">, {
|
|
132
|
+
}, "required", "userId" | "actionName" | "tags" | "model" | "estimatedNanos" | "estimatedTokens" | "unpricedModel" | "overBudget" | "settled" | "messages" | "status" | "error" | "responseText" | "promptTokens" | "completionTokens" | "cachedTokens" | "costNanos" | "latencyMs" | "rerunOf">, {
|
|
156
133
|
userId: ["userId", "_creationTime"];
|
|
157
134
|
status: ["status", "_creationTime"];
|
|
158
135
|
rerunOf: ["rerunOf", "_creationTime"];
|
package/dist/component/schema.js
CHANGED
|
@@ -4,20 +4,28 @@ export const vMessage = v.object({
|
|
|
4
4
|
role: v.string(), // "system" | "user" | "assistant" | "tool"
|
|
5
5
|
content: v.string(),
|
|
6
6
|
});
|
|
7
|
+
// One attribution tag: a (dimension, value) pair, e.g. {dimension:"user",
|
|
8
|
+
// value:"alice"} or {dimension:"customer", value:"acme"}. `user` and `action`
|
|
9
|
+
// are built-in dimensions; apps can add any others (team, project, env, …).
|
|
10
|
+
export const vTag = v.object({ dimension: v.string(), value: v.string() });
|
|
7
11
|
export default defineSchema({
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
// A budget holder, keyed by (dimension, value). Unifies what used to be the
|
|
13
|
+
// `users` and `actions` tables — those are just the "user" and "action"
|
|
14
|
+
// dimensions now. Any tag a request carries can have its own budget here.
|
|
15
|
+
buckets: defineTable({
|
|
16
|
+
dimension: v.string(),
|
|
17
|
+
value: v.string(),
|
|
10
18
|
// limits (all optional — unlimited by default)
|
|
11
|
-
requestsPerMinute: v.optional(v.number()),
|
|
19
|
+
requestsPerMinute: v.optional(v.number()), // enforced on the "user" dimension
|
|
12
20
|
dailySpendLimitNanos: v.optional(v.number()),
|
|
13
21
|
lifetimeSpendLimitNanos: v.optional(v.number()),
|
|
14
22
|
dailyTokenLimit: v.optional(v.number()),
|
|
15
23
|
lifetimeTokenLimit: v.optional(v.number()),
|
|
16
|
-
blocked: v.optional(v.boolean()),
|
|
24
|
+
blocked: v.optional(v.boolean()), // hard block (was `blocked`/`disabled`)
|
|
17
25
|
// "hard" (default): exceeding a budget blocks. "soft": warn but allow.
|
|
18
26
|
enforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
|
|
19
|
-
// one-time bumps ("approve another $X")
|
|
20
|
-
//
|
|
27
|
+
// one-time bumps ("approve another $X"). Daily bump is scoped to bumpDayStamp
|
|
28
|
+
// (resets with the day); lifetime bump is permanent.
|
|
21
29
|
dailyBumpNanos: v.optional(v.number()),
|
|
22
30
|
lifetimeBumpNanos: v.optional(v.number()),
|
|
23
31
|
bumpDayStamp: v.optional(v.string()),
|
|
@@ -26,43 +34,24 @@ export default defineSchema({
|
|
|
26
34
|
totalRequests: v.number(),
|
|
27
35
|
totalTokens: v.number(),
|
|
28
36
|
// daily window
|
|
29
|
-
dayStamp: v.string(), // e.g. "2026-08-27" (UTC)
|
|
30
|
-
spendTodayNanos: v.number(),
|
|
31
|
-
tokensToday: v.optional(v.number()),
|
|
32
|
-
// in-flight reservations (pessimistic holds; released on settle/expiry)
|
|
33
|
-
reservedTodayNanos: v.optional(v.number()),
|
|
34
|
-
reservedTotalNanos: v.optional(v.number()),
|
|
35
|
-
reservedTodayTokens: v.optional(v.number()),
|
|
36
|
-
reservedTotalTokens: v.optional(v.number()),
|
|
37
|
-
pendingCount: v.optional(v.number()),
|
|
38
|
-
}).index("userId", ["userId"]),
|
|
39
|
-
// per-action-name budgets and running totals (e.g. "chat", "summarize")
|
|
40
|
-
actions: defineTable({
|
|
41
|
-
name: v.string(),
|
|
42
|
-
dailySpendLimitNanos: v.optional(v.number()),
|
|
43
|
-
lifetimeSpendLimitNanos: v.optional(v.number()),
|
|
44
|
-
dailyTokenLimit: v.optional(v.number()),
|
|
45
|
-
lifetimeTokenLimit: v.optional(v.number()),
|
|
46
|
-
disabled: v.optional(v.boolean()),
|
|
47
|
-
enforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
|
|
48
|
-
dailyBumpNanos: v.optional(v.number()),
|
|
49
|
-
lifetimeBumpNanos: v.optional(v.number()),
|
|
50
|
-
bumpDayStamp: v.optional(v.string()),
|
|
51
|
-
totalSpendNanos: v.number(),
|
|
52
|
-
totalRequests: v.number(),
|
|
53
|
-
totalTokens: v.number(),
|
|
54
37
|
dayStamp: v.string(),
|
|
55
38
|
spendTodayNanos: v.number(),
|
|
56
39
|
tokensToday: v.optional(v.number()),
|
|
40
|
+
// in-flight reservations (pessimistic holds; released on settle/expiry)
|
|
57
41
|
reservedTodayNanos: v.optional(v.number()),
|
|
58
42
|
reservedTotalNanos: v.optional(v.number()),
|
|
59
43
|
reservedTodayTokens: v.optional(v.number()),
|
|
60
44
|
reservedTotalTokens: v.optional(v.number()),
|
|
61
45
|
pendingCount: v.optional(v.number()),
|
|
62
|
-
})
|
|
46
|
+
})
|
|
47
|
+
.index("dim_value", ["dimension", "value"])
|
|
48
|
+
.index("dimension", ["dimension"]),
|
|
63
49
|
requests: defineTable({
|
|
50
|
+
// `user` and `action` stay first-class + indexed (the hot-path filters and
|
|
51
|
+
// rate limiting); the full attribution incl. extra tags lives in `tags`.
|
|
64
52
|
userId: v.string(),
|
|
65
53
|
actionName: v.optional(v.string()),
|
|
54
|
+
tags: v.optional(v.array(vTag)),
|
|
66
55
|
model: v.string(),
|
|
67
56
|
// pessimistic holds placed at start; reconciled to actual on settle
|
|
68
57
|
estimatedNanos: v.optional(v.number()),
|
|
@@ -105,12 +94,12 @@ export default defineSchema({
|
|
|
105
94
|
// "denylist": any model except the listed ones.
|
|
106
95
|
modelMode: v.optional(v.union(v.literal("open"), v.literal("allowlist"), v.literal("denylist"))),
|
|
107
96
|
models: v.optional(v.array(v.string())),
|
|
108
|
-
// Deployment-wide ("global") spend cap across ALL
|
|
109
|
-
//
|
|
110
|
-
// the limit config lives here.
|
|
111
|
-
// sharded total is read without a reservation, so under heavy
|
|
112
|
-
// it can overshoot by a bounded amount. Right for a global
|
|
113
|
-
// per-
|
|
97
|
+
// Deployment-wide ("global") spend cap across ALL requests. Running totals
|
|
98
|
+
// live in a sharded counter (high write throughput) since every request
|
|
99
|
+
// touches it; only the limit config lives here. Enforced approximately —
|
|
100
|
+
// the sharded total is read without a reservation, so under heavy
|
|
101
|
+
// concurrency it can overshoot by a bounded amount. Right for a global
|
|
102
|
+
// killswitch; per-bucket caps stay exact via reserve/settle.
|
|
114
103
|
globalDailySpendLimitNanos: v.optional(v.number()),
|
|
115
104
|
globalLifetimeSpendLimitNanos: v.optional(v.number()),
|
|
116
105
|
globalEnforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"email": "support@convex.dev",
|
|
8
8
|
"url": "https://github.com/get-convex/ai-budget/issues"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.0.2-alpha.
|
|
10
|
+
"version": "0.0.2-alpha.4",
|
|
11
11
|
"license": "Apache-2.0",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"keywords": [
|
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
"convex": "^1.45.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@edge-runtime/vm": "
|
|
68
|
-
"convex": "
|
|
69
|
-
"convex-test": "
|
|
67
|
+
"@edge-runtime/vm": "5.0.0",
|
|
68
|
+
"convex": "1.45.0",
|
|
69
|
+
"convex-test": "0.0.56",
|
|
70
70
|
"typescript": "^5.7.0",
|
|
71
|
-
"vitest": "
|
|
71
|
+
"vitest": "4.1.11"
|
|
72
72
|
}
|
|
73
73
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -41,10 +41,19 @@ export type AIBudgetApi = UseApi<typeof api>;
|
|
|
41
41
|
/** @deprecated use AIBudgetApi */
|
|
42
42
|
export type AIGatewayApi = AIBudgetApi;
|
|
43
43
|
|
|
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
|
+
|
|
44
52
|
/** Fired when a request is admitted over a *soft* limit. */
|
|
45
53
|
export type SoftLimitInfo = {
|
|
46
54
|
userId: string;
|
|
47
55
|
action?: string;
|
|
56
|
+
tags?: Tag[];
|
|
48
57
|
requestId: string;
|
|
49
58
|
warnings: string[];
|
|
50
59
|
};
|
|
@@ -213,6 +222,8 @@ export class AIBudget {
|
|
|
213
222
|
rerunOf?: string;
|
|
214
223
|
/** Attribute spend to this action name. Defaults to the calling Convex action. */
|
|
215
224
|
action?: string;
|
|
225
|
+
/** Extra attribution dimensions to bill/limit (team, customer, env, …). */
|
|
226
|
+
tags?: Tag[];
|
|
216
227
|
} = {}
|
|
217
228
|
): Promise<ChatResult> {
|
|
218
229
|
const model = args.model ?? this.defaultModel;
|
|
@@ -223,6 +234,7 @@ export class AIBudget {
|
|
|
223
234
|
const started = await ctx.runMutation(this.component.lib.startRequest, {
|
|
224
235
|
userId,
|
|
225
236
|
actionName,
|
|
237
|
+
tags: args.tags,
|
|
226
238
|
model,
|
|
227
239
|
messages,
|
|
228
240
|
rerunOf: args.rerunOf as any,
|
|
@@ -236,7 +248,13 @@ export class AIBudget {
|
|
|
236
248
|
}
|
|
237
249
|
const requestId = started.requestId;
|
|
238
250
|
const warnings = started.warnings;
|
|
239
|
-
await this.fireSoftLimit({
|
|
251
|
+
await this.fireSoftLimit({
|
|
252
|
+
userId,
|
|
253
|
+
action: actionName,
|
|
254
|
+
tags: args.tags,
|
|
255
|
+
requestId,
|
|
256
|
+
warnings,
|
|
257
|
+
});
|
|
240
258
|
const start = Date.now();
|
|
241
259
|
try {
|
|
242
260
|
// The full chain (incl. system) is stored on the request for audit/replay,
|
|
@@ -281,7 +299,13 @@ export class AIBudget {
|
|
|
281
299
|
*/
|
|
282
300
|
languageModel(
|
|
283
301
|
ctx: RunMutationCtx,
|
|
284
|
-
opts: {
|
|
302
|
+
opts: {
|
|
303
|
+
userId?: string;
|
|
304
|
+
model?: string;
|
|
305
|
+
action?: string;
|
|
306
|
+
/** Extra attribution dimensions to bill/limit (team, customer, env, …). */
|
|
307
|
+
tags?: Tag[];
|
|
308
|
+
} = {}
|
|
285
309
|
): LanguageModel {
|
|
286
310
|
const modelId = opts.model ?? this.defaultModel;
|
|
287
311
|
const component = this.component;
|
|
@@ -293,6 +317,7 @@ export class AIBudget {
|
|
|
293
317
|
const started = await ctx.runMutation(component.lib.startRequest, {
|
|
294
318
|
userId,
|
|
295
319
|
actionName,
|
|
320
|
+
tags: opts.tags,
|
|
296
321
|
model: modelId,
|
|
297
322
|
messages: simplifyPrompt(params.prompt),
|
|
298
323
|
});
|
|
@@ -306,6 +331,7 @@ export class AIBudget {
|
|
|
306
331
|
await fireSoftLimit({
|
|
307
332
|
userId,
|
|
308
333
|
action: actionName,
|
|
334
|
+
tags: opts.tags,
|
|
309
335
|
requestId: started.requestId,
|
|
310
336
|
warnings: started.warnings,
|
|
311
337
|
});
|
|
@@ -409,6 +435,7 @@ export class AIBudget {
|
|
|
409
435
|
messages: args.messages ?? original.messages,
|
|
410
436
|
rerunOf: args.requestId,
|
|
411
437
|
action: original.actionName,
|
|
438
|
+
tags: original.tags,
|
|
412
439
|
});
|
|
413
440
|
}
|
|
414
441
|
|
|
@@ -431,11 +458,55 @@ export class AIBudget {
|
|
|
431
458
|
};
|
|
432
459
|
}
|
|
433
460
|
|
|
434
|
-
/**
|
|
461
|
+
/**
|
|
462
|
+
* Budgets and controls for an arbitrary attribution dimension — the
|
|
463
|
+
* generalization of `users`/`actions`. Give it any dimension name (team,
|
|
464
|
+
* project, tenant, customer, env, feature, …) and set caps per value:
|
|
465
|
+
*
|
|
466
|
+
* ai.tag("customer").setLimits(ctx, { value: "acme", dailySpendLimitNanos });
|
|
467
|
+
* ai.tag("customer").list(ctx);
|
|
468
|
+
*
|
|
469
|
+
* Attribute a call to it by passing `tags` to `chat`/`languageModel`.
|
|
470
|
+
*/
|
|
471
|
+
tag(dimension: string) {
|
|
472
|
+
const c = this.component;
|
|
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
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Per-user budgets and controls — sugar over the "user" dimension. */
|
|
435
505
|
get users() {
|
|
436
506
|
const c = this.component;
|
|
437
507
|
return {
|
|
438
|
-
list: (ctx: RunQueryCtx) =>
|
|
508
|
+
list: (ctx: RunQueryCtx) =>
|
|
509
|
+
ctx.runQuery(c.lib.listBuckets, { dimension: "user" }),
|
|
439
510
|
setLimits: (
|
|
440
511
|
ctx: RunMutationCtx,
|
|
441
512
|
args: {
|
|
@@ -448,23 +519,40 @@ export class AIBudget {
|
|
|
448
519
|
enforcement?: "hard" | "soft";
|
|
449
520
|
blocked?: boolean;
|
|
450
521
|
}
|
|
451
|
-
) =>
|
|
522
|
+
) => {
|
|
523
|
+
const { userId, ...limits } = args;
|
|
524
|
+
return ctx.runMutation(c.lib.setBucketLimits, {
|
|
525
|
+
dimension: "user",
|
|
526
|
+
value: userId,
|
|
527
|
+
...limits,
|
|
528
|
+
});
|
|
529
|
+
},
|
|
452
530
|
/** One-time "approve another $X" bump (daily is today-only). */
|
|
453
531
|
bump: (
|
|
454
532
|
ctx: RunMutationCtx,
|
|
455
533
|
args: { userId: string; dailyNanos?: number; lifetimeNanos?: number }
|
|
456
|
-
) =>
|
|
534
|
+
) =>
|
|
535
|
+
ctx.runMutation(c.lib.bumpBucket, {
|
|
536
|
+
dimension: "user",
|
|
537
|
+
value: args.userId,
|
|
538
|
+
dailyNanos: args.dailyNanos,
|
|
539
|
+
lifetimeNanos: args.lifetimeNanos,
|
|
540
|
+
}),
|
|
457
541
|
/** Delete a user and all their request rows. */
|
|
458
542
|
delete: (ctx: RunMutationCtx, args: { userId: string }) =>
|
|
459
|
-
ctx.runMutation(c.lib.
|
|
543
|
+
ctx.runMutation(c.lib.deleteBucket, {
|
|
544
|
+
dimension: "user",
|
|
545
|
+
value: args.userId,
|
|
546
|
+
}),
|
|
460
547
|
};
|
|
461
548
|
}
|
|
462
549
|
|
|
463
|
-
/** Per-action (per-feature) budgets. */
|
|
550
|
+
/** Per-action (per-feature) budgets — sugar over the "action" dimension. */
|
|
464
551
|
get actions() {
|
|
465
552
|
const c = this.component;
|
|
466
553
|
return {
|
|
467
|
-
list: (ctx: RunQueryCtx) =>
|
|
554
|
+
list: (ctx: RunQueryCtx) =>
|
|
555
|
+
ctx.runQuery(c.lib.listBuckets, { dimension: "action" }),
|
|
468
556
|
setLimits: (
|
|
469
557
|
ctx: RunMutationCtx,
|
|
470
558
|
args: {
|
|
@@ -474,13 +562,27 @@ export class AIBudget {
|
|
|
474
562
|
dailyTokenLimit?: number;
|
|
475
563
|
lifetimeTokenLimit?: number;
|
|
476
564
|
enforcement?: "hard" | "soft";
|
|
477
|
-
disabled
|
|
565
|
+
/** Hard-block this action (was `disabled`). */
|
|
566
|
+
blocked?: boolean;
|
|
478
567
|
}
|
|
479
|
-
) =>
|
|
568
|
+
) => {
|
|
569
|
+
const { name, ...limits } = args;
|
|
570
|
+
return ctx.runMutation(c.lib.setBucketLimits, {
|
|
571
|
+
dimension: "action",
|
|
572
|
+
value: name,
|
|
573
|
+
...limits,
|
|
574
|
+
});
|
|
575
|
+
},
|
|
480
576
|
bump: (
|
|
481
577
|
ctx: RunMutationCtx,
|
|
482
578
|
args: { name: string; dailyNanos?: number; lifetimeNanos?: number }
|
|
483
|
-
) =>
|
|
579
|
+
) =>
|
|
580
|
+
ctx.runMutation(c.lib.bumpBucket, {
|
|
581
|
+
dimension: "action",
|
|
582
|
+
value: args.name,
|
|
583
|
+
dailyNanos: args.dailyNanos,
|
|
584
|
+
lifetimeNanos: args.lifetimeNanos,
|
|
585
|
+
}),
|
|
484
586
|
};
|
|
485
587
|
}
|
|
486
588
|
|
|
@@ -24,10 +24,15 @@ import type { FunctionReference } from "convex/server";
|
|
|
24
24
|
export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
25
25
|
{
|
|
26
26
|
lib: {
|
|
27
|
-
|
|
27
|
+
bumpBucket: FunctionReference<
|
|
28
28
|
"mutation",
|
|
29
29
|
"internal",
|
|
30
|
-
{
|
|
30
|
+
{
|
|
31
|
+
dailyNanos?: number;
|
|
32
|
+
dimension: string;
|
|
33
|
+
lifetimeNanos?: number;
|
|
34
|
+
value: string;
|
|
35
|
+
},
|
|
31
36
|
null,
|
|
32
37
|
Name
|
|
33
38
|
>;
|
|
@@ -38,17 +43,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
38
43
|
null,
|
|
39
44
|
Name
|
|
40
45
|
>;
|
|
41
|
-
|
|
46
|
+
deleteBucket: FunctionReference<
|
|
42
47
|
"mutation",
|
|
43
48
|
"internal",
|
|
44
|
-
{
|
|
45
|
-
null,
|
|
46
|
-
Name
|
|
47
|
-
>;
|
|
48
|
-
deleteUser: FunctionReference<
|
|
49
|
-
"mutation",
|
|
50
|
-
"internal",
|
|
51
|
-
{ userId: string },
|
|
49
|
+
{ dimension: string; value: string },
|
|
52
50
|
{ deletedThisBatch: number; done: boolean },
|
|
53
51
|
Name
|
|
54
52
|
>;
|
|
@@ -67,6 +65,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
67
65
|
{ costNanos: number },
|
|
68
66
|
Name
|
|
69
67
|
>;
|
|
68
|
+
getBucket: FunctionReference<
|
|
69
|
+
"query",
|
|
70
|
+
"internal",
|
|
71
|
+
{ dimension: string; value: string },
|
|
72
|
+
any,
|
|
73
|
+
Name
|
|
74
|
+
>;
|
|
70
75
|
getGlobalStatus: FunctionReference<
|
|
71
76
|
"query",
|
|
72
77
|
"internal",
|
|
@@ -101,7 +106,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
101
106
|
any,
|
|
102
107
|
Name
|
|
103
108
|
>;
|
|
104
|
-
|
|
109
|
+
listBuckets: FunctionReference<
|
|
110
|
+
"query",
|
|
111
|
+
"internal",
|
|
112
|
+
{ dimension?: string },
|
|
113
|
+
any,
|
|
114
|
+
Name
|
|
115
|
+
>;
|
|
105
116
|
listPrices: FunctionReference<"query", "internal", {}, any, Name>;
|
|
106
117
|
listRequests: FunctionReference<
|
|
107
118
|
"query",
|
|
@@ -110,18 +121,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
110
121
|
any,
|
|
111
122
|
Name
|
|
112
123
|
>;
|
|
113
|
-
|
|
114
|
-
setActionLimits: FunctionReference<
|
|
124
|
+
setBucketLimits: FunctionReference<
|
|
115
125
|
"mutation",
|
|
116
126
|
"internal",
|
|
117
127
|
{
|
|
128
|
+
blocked?: boolean;
|
|
118
129
|
dailySpendLimitNanos?: number;
|
|
119
130
|
dailyTokenLimit?: number;
|
|
120
|
-
|
|
131
|
+
dimension: string;
|
|
121
132
|
enforcement?: "hard" | "soft";
|
|
122
133
|
lifetimeSpendLimitNanos?: number;
|
|
123
134
|
lifetimeTokenLimit?: number;
|
|
124
|
-
|
|
135
|
+
requestsPerMinute?: number;
|
|
136
|
+
value: string;
|
|
125
137
|
},
|
|
126
138
|
null,
|
|
127
139
|
Name
|
|
@@ -137,22 +149,6 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
137
149
|
null,
|
|
138
150
|
Name
|
|
139
151
|
>;
|
|
140
|
-
setLimits: FunctionReference<
|
|
141
|
-
"mutation",
|
|
142
|
-
"internal",
|
|
143
|
-
{
|
|
144
|
-
blocked?: boolean;
|
|
145
|
-
dailySpendLimitNanos?: number;
|
|
146
|
-
dailyTokenLimit?: number;
|
|
147
|
-
enforcement?: "hard" | "soft";
|
|
148
|
-
lifetimeSpendLimitNanos?: number;
|
|
149
|
-
lifetimeTokenLimit?: number;
|
|
150
|
-
requestsPerMinute?: number;
|
|
151
|
-
userId: string;
|
|
152
|
-
},
|
|
153
|
-
null,
|
|
154
|
-
Name
|
|
155
|
-
>;
|
|
156
152
|
setModelPolicy: FunctionReference<
|
|
157
153
|
"mutation",
|
|
158
154
|
"internal",
|
|
@@ -186,6 +182,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
186
182
|
messages: Array<{ content: string; role: string }>;
|
|
187
183
|
model: string;
|
|
188
184
|
rerunOf?: string;
|
|
185
|
+
tags?: Array<{ dimension: string; value: string }>;
|
|
189
186
|
userId: string;
|
|
190
187
|
},
|
|
191
188
|
| { allowed: true; requestId: string; warnings: Array<string> }
|