@convex-dev/ai-budget 0.0.2-alpha.0

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.
@@ -0,0 +1,199 @@
1
+ export declare const vMessage: import("convex/values").VObject<{
2
+ role: string;
3
+ content: string;
4
+ }, {
5
+ role: import("convex/values").VString<string, "required">;
6
+ content: import("convex/values").VString<string, "required">;
7
+ }, "required", "role" | "content">;
8
+ declare const _default: import("convex/server").SchemaDefinition<{
9
+ users: import("convex/server").TableDefinition<import("convex/values").VObject<{
10
+ requestsPerMinute?: number | undefined;
11
+ dailySpendLimitNanos?: number | undefined;
12
+ lifetimeSpendLimitNanos?: number | undefined;
13
+ dailyTokenLimit?: number | undefined;
14
+ lifetimeTokenLimit?: number | undefined;
15
+ blocked?: boolean | undefined;
16
+ enforcement?: "hard" | "soft" | undefined;
17
+ dailyBumpNanos?: number | undefined;
18
+ lifetimeBumpNanos?: number | undefined;
19
+ bumpDayStamp?: string | undefined;
20
+ tokensToday?: number | undefined;
21
+ reservedTodayNanos?: number | undefined;
22
+ reservedTotalNanos?: number | undefined;
23
+ reservedTodayTokens?: number | undefined;
24
+ reservedTotalTokens?: number | undefined;
25
+ pendingCount?: number | undefined;
26
+ userId: string;
27
+ totalSpendNanos: number;
28
+ totalRequests: number;
29
+ totalTokens: number;
30
+ dayStamp: string;
31
+ spendTodayNanos: number;
32
+ }, {
33
+ userId: import("convex/values").VString<string, "required">;
34
+ requestsPerMinute: import("convex/values").VFloat64<number | undefined, "optional">;
35
+ dailySpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
36
+ lifetimeSpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
37
+ dailyTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
38
+ lifetimeTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
39
+ blocked: import("convex/values").VBoolean<boolean | undefined, "optional">;
40
+ enforcement: import("convex/values").VUnion<"hard" | "soft" | undefined, [import("convex/values").VLiteral<"hard", "required">, import("convex/values").VLiteral<"soft", "required">], "optional", never>;
41
+ dailyBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
42
+ lifetimeBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
43
+ bumpDayStamp: import("convex/values").VString<string | undefined, "optional">;
44
+ totalSpendNanos: import("convex/values").VFloat64<number, "required">;
45
+ totalRequests: import("convex/values").VFloat64<number, "required">;
46
+ totalTokens: import("convex/values").VFloat64<number, "required">;
47
+ dayStamp: import("convex/values").VString<string, "required">;
48
+ spendTodayNanos: import("convex/values").VFloat64<number, "required">;
49
+ tokensToday: import("convex/values").VFloat64<number | undefined, "optional">;
50
+ reservedTodayNanos: import("convex/values").VFloat64<number | undefined, "optional">;
51
+ reservedTotalNanos: import("convex/values").VFloat64<number | undefined, "optional">;
52
+ reservedTodayTokens: import("convex/values").VFloat64<number | undefined, "optional">;
53
+ reservedTotalTokens: import("convex/values").VFloat64<number | undefined, "optional">;
54
+ pendingCount: import("convex/values").VFloat64<number | undefined, "optional">;
55
+ }, "required", "userId" | "requestsPerMinute" | "dailySpendLimitNanos" | "lifetimeSpendLimitNanos" | "dailyTokenLimit" | "lifetimeTokenLimit" | "blocked" | "enforcement" | "dailyBumpNanos" | "lifetimeBumpNanos" | "bumpDayStamp" | "totalSpendNanos" | "totalRequests" | "totalTokens" | "dayStamp" | "spendTodayNanos" | "tokensToday" | "reservedTodayNanos" | "reservedTotalNanos" | "reservedTodayTokens" | "reservedTotalTokens" | "pendingCount">, {
56
+ userId: ["userId", "_creationTime"];
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"];
104
+ }, {}, {}>;
105
+ requests: import("convex/server").TableDefinition<import("convex/values").VObject<{
106
+ actionName?: string | undefined;
107
+ estimatedNanos?: number | undefined;
108
+ estimatedTokens?: number | undefined;
109
+ unpricedModel?: boolean | undefined;
110
+ overBudget?: boolean | undefined;
111
+ settled?: boolean | undefined;
112
+ error?: string | undefined;
113
+ responseText?: string | undefined;
114
+ promptTokens?: number | undefined;
115
+ completionTokens?: number | undefined;
116
+ cachedTokens?: number | undefined;
117
+ costNanos?: number | undefined;
118
+ latencyMs?: number | undefined;
119
+ rerunOf?: import("convex/values").GenericId<"requests"> | undefined;
120
+ userId: string;
121
+ model: string;
122
+ messages: {
123
+ role: string;
124
+ content: string;
125
+ }[];
126
+ status: "blocked" | "pending" | "success" | "error";
127
+ }, {
128
+ userId: import("convex/values").VString<string, "required">;
129
+ actionName: import("convex/values").VString<string | undefined, "optional">;
130
+ model: import("convex/values").VString<string, "required">;
131
+ estimatedNanos: import("convex/values").VFloat64<number | undefined, "optional">;
132
+ estimatedTokens: import("convex/values").VFloat64<number | undefined, "optional">;
133
+ unpricedModel: import("convex/values").VBoolean<boolean | undefined, "optional">;
134
+ overBudget: import("convex/values").VBoolean<boolean | undefined, "optional">;
135
+ settled: import("convex/values").VBoolean<boolean | undefined, "optional">;
136
+ messages: import("convex/values").VArray<{
137
+ role: string;
138
+ content: string;
139
+ }[], import("convex/values").VObject<{
140
+ role: string;
141
+ content: string;
142
+ }, {
143
+ role: import("convex/values").VString<string, "required">;
144
+ content: import("convex/values").VString<string, "required">;
145
+ }, "required", "role" | "content">, "required">;
146
+ status: import("convex/values").VUnion<"blocked" | "pending" | "success" | "error", [import("convex/values").VLiteral<"pending", "required">, import("convex/values").VLiteral<"success", "required">, import("convex/values").VLiteral<"error", "required">, import("convex/values").VLiteral<"blocked", "required">], "required", never>;
147
+ responseText: import("convex/values").VString<string | undefined, "optional">;
148
+ error: import("convex/values").VString<string | undefined, "optional">;
149
+ promptTokens: import("convex/values").VFloat64<number | undefined, "optional">;
150
+ completionTokens: import("convex/values").VFloat64<number | undefined, "optional">;
151
+ cachedTokens: import("convex/values").VFloat64<number | undefined, "optional">;
152
+ costNanos: import("convex/values").VFloat64<number | undefined, "optional">;
153
+ latencyMs: import("convex/values").VFloat64<number | undefined, "optional">;
154
+ 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">, {
156
+ userId: ["userId", "_creationTime"];
157
+ status: ["status", "_creationTime"];
158
+ rerunOf: ["rerunOf", "_creationTime"];
159
+ actionName: ["actionName", "_creationTime"];
160
+ settled: ["settled", "_creationTime"];
161
+ }, {}, {}>;
162
+ prices: import("convex/server").TableDefinition<import("convex/values").VObject<{
163
+ model: string;
164
+ inputNanosPerMTok: number;
165
+ outputNanosPerMTok: number;
166
+ }, {
167
+ model: import("convex/values").VString<string, "required">;
168
+ inputNanosPerMTok: import("convex/values").VFloat64<number, "required">;
169
+ outputNanosPerMTok: import("convex/values").VFloat64<number, "required">;
170
+ }, "required", "model" | "inputNanosPerMTok" | "outputNanosPerMTok">, {
171
+ model: ["model", "_creationTime"];
172
+ }, {}, {}>;
173
+ settings: import("convex/server").TableDefinition<import("convex/values").VObject<{
174
+ modelMode?: "open" | "allowlist" | "denylist" | undefined;
175
+ models?: string[] | undefined;
176
+ globalDailySpendLimitNanos?: number | undefined;
177
+ globalLifetimeSpendLimitNanos?: number | undefined;
178
+ globalEnforcement?: "hard" | "soft" | undefined;
179
+ globalDailyBumpNanos?: number | undefined;
180
+ globalLifetimeBumpNanos?: number | undefined;
181
+ globalBumpDayStamp?: string | undefined;
182
+ retentionMs?: number | undefined;
183
+ key: string;
184
+ }, {
185
+ key: import("convex/values").VString<string, "required">;
186
+ modelMode: import("convex/values").VUnion<"open" | "allowlist" | "denylist" | undefined, [import("convex/values").VLiteral<"open", "required">, import("convex/values").VLiteral<"allowlist", "required">, import("convex/values").VLiteral<"denylist", "required">], "optional", never>;
187
+ models: import("convex/values").VArray<string[] | undefined, import("convex/values").VString<string, "required">, "optional">;
188
+ globalDailySpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
189
+ globalLifetimeSpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
190
+ globalEnforcement: import("convex/values").VUnion<"hard" | "soft" | undefined, [import("convex/values").VLiteral<"hard", "required">, import("convex/values").VLiteral<"soft", "required">], "optional", never>;
191
+ globalDailyBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
192
+ globalLifetimeBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
193
+ globalBumpDayStamp: import("convex/values").VString<string | undefined, "optional">;
194
+ retentionMs: import("convex/values").VFloat64<number | undefined, "optional">;
195
+ }, "required", "key" | "modelMode" | "models" | "globalDailySpendLimitNanos" | "globalLifetimeSpendLimitNanos" | "globalEnforcement" | "globalDailyBumpNanos" | "globalLifetimeBumpNanos" | "globalBumpDayStamp" | "retentionMs">, {
196
+ key: ["key", "_creationTime"];
197
+ }, {}, {}>;
198
+ }, true>;
199
+ export default _default;
@@ -0,0 +1,123 @@
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+ export const vMessage = v.object({
4
+ role: v.string(), // "system" | "user" | "assistant" | "tool"
5
+ content: v.string(),
6
+ });
7
+ export default defineSchema({
8
+ users: defineTable({
9
+ userId: v.string(), // app-provided stable key (your user id)
10
+ // limits (all optional — unlimited by default)
11
+ requestsPerMinute: v.optional(v.number()),
12
+ dailySpendLimitNanos: v.optional(v.number()),
13
+ lifetimeSpendLimitNanos: v.optional(v.number()),
14
+ dailyTokenLimit: v.optional(v.number()),
15
+ lifetimeTokenLimit: v.optional(v.number()),
16
+ blocked: v.optional(v.boolean()),
17
+ // "hard" (default): exceeding a budget blocks. "soft": warn but allow.
18
+ enforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
19
+ // one-time bumps ("approve another $X") added on top of the cap. Daily bump
20
+ // is scoped to bumpDayStamp (resets with the day); lifetime bump is permanent.
21
+ dailyBumpNanos: v.optional(v.number()),
22
+ lifetimeBumpNanos: v.optional(v.number()),
23
+ bumpDayStamp: v.optional(v.string()),
24
+ // settled totals (from finished requests)
25
+ totalSpendNanos: v.number(),
26
+ totalRequests: v.number(),
27
+ totalTokens: v.number(),
28
+ // 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
+ dayStamp: v.string(),
55
+ spendTodayNanos: v.number(),
56
+ tokensToday: v.optional(v.number()),
57
+ reservedTodayNanos: v.optional(v.number()),
58
+ reservedTotalNanos: v.optional(v.number()),
59
+ reservedTodayTokens: v.optional(v.number()),
60
+ reservedTotalTokens: v.optional(v.number()),
61
+ pendingCount: v.optional(v.number()),
62
+ }).index("name", ["name"]),
63
+ requests: defineTable({
64
+ userId: v.string(),
65
+ actionName: v.optional(v.string()),
66
+ model: v.string(),
67
+ // pessimistic holds placed at start; reconciled to actual on settle
68
+ estimatedNanos: v.optional(v.number()),
69
+ estimatedTokens: v.optional(v.number()),
70
+ // true when the model had no known/override price and was charged the
71
+ // conservative fallback — a signal to add a real price via setPrice.
72
+ unpricedModel: v.optional(v.boolean()),
73
+ // true when a soft budget was exceeded (allowed with a warning).
74
+ overBudget: v.optional(v.boolean()),
75
+ // false once finished and awaiting fold into totals; true once folded.
76
+ // absent while pending or blocked (so the reconciler ignores those).
77
+ settled: v.optional(v.boolean()),
78
+ messages: v.array(vMessage),
79
+ status: v.union(v.literal("pending"), v.literal("success"), v.literal("error"), v.literal("blocked")),
80
+ responseText: v.optional(v.string()),
81
+ error: v.optional(v.string()),
82
+ promptTokens: v.optional(v.number()),
83
+ completionTokens: v.optional(v.number()),
84
+ // subset of promptTokens served from the provider's prompt cache (cheaper).
85
+ cachedTokens: v.optional(v.number()),
86
+ costNanos: v.optional(v.number()),
87
+ latencyMs: v.optional(v.number()),
88
+ rerunOf: v.optional(v.id("requests")),
89
+ })
90
+ .index("userId", ["userId"])
91
+ .index("status", ["status"])
92
+ .index("rerunOf", ["rerunOf"])
93
+ .index("actionName", ["actionName"])
94
+ .index("settled", ["settled"]),
95
+ // per-model price overrides (nanodollars per million tokens)
96
+ prices: defineTable({
97
+ model: v.string(),
98
+ inputNanosPerMTok: v.number(),
99
+ outputNanosPerMTok: v.number(),
100
+ }).index("model", ["model"]),
101
+ // singleton component config (key === "singleton")
102
+ settings: defineTable({
103
+ key: v.string(),
104
+ // "open": any model allowed. "allowlist": only listed models.
105
+ // "denylist": any model except the listed ones.
106
+ modelMode: v.optional(v.union(v.literal("open"), v.literal("allowlist"), v.literal("denylist"))),
107
+ models: v.optional(v.array(v.string())),
108
+ // Deployment-wide ("global") spend cap across ALL users and actions. The
109
+ // running totals live in a sharded counter (high write throughput); only
110
+ // the limit config lives here. The cap is enforced approximately — the
111
+ // sharded total is read without a reservation, so under heavy concurrency
112
+ // it can overshoot by a bounded amount. Right for a global killswitch;
113
+ // per-user/per-action caps stay exact via reserve/settle.
114
+ globalDailySpendLimitNanos: v.optional(v.number()),
115
+ globalLifetimeSpendLimitNanos: v.optional(v.number()),
116
+ globalEnforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
117
+ globalDailyBumpNanos: v.optional(v.number()),
118
+ globalLifetimeBumpNanos: v.optional(v.number()),
119
+ globalBumpDayStamp: v.optional(v.string()),
120
+ // request-row retention window in ms (default 1h); 0 disables sweeping.
121
+ retentionMs: v.optional(v.number()),
122
+ }).index("key", ["key"]),
123
+ });
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@convex-dev/ai-budget",
3
+ "description": "Metered, budget-governed AI Gateway usage for Convex: cost & usage tracking, per-user/per-action/global spend/token/rate limits, request logging, re-run, and an Agent-component adapter.",
4
+ "repository": "github:get-convex/ai-budget",
5
+ "homepage": "https://github.com/get-convex/ai-budget#readme",
6
+ "bugs": {
7
+ "email": "support@convex.dev",
8
+ "url": "https://github.com/get-convex/ai-budget/issues"
9
+ },
10
+ "version": "0.0.2-alpha.0",
11
+ "license": "Apache-2.0",
12
+ "type": "module",
13
+ "keywords": [
14
+ "convex",
15
+ "convex-component",
16
+ "ai-gateway",
17
+ "llm",
18
+ "usage",
19
+ "cost",
20
+ "budget",
21
+ "rate-limit"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc --project ./tsconfig.build.json",
25
+ "build:codegen": "convex codegen --component-dir ./src/component && npm run build",
26
+ "build:clean": "rm -rf dist *.tsbuildinfo && npm run build:codegen",
27
+ "typecheck": "tsc --noEmit --project ./tsconfig.build.json && tsc --noEmit -p example",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "prepack": "npm run build",
31
+ "alpha": "npm version prerelease --preid alpha && git push --follow-tags",
32
+ "release": "npm version patch && git push --follow-tags"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src"
37
+ ],
38
+ "exports": {
39
+ "./package.json": "./package.json",
40
+ ".": {
41
+ "types": "./dist/client/index.d.ts",
42
+ "default": "./dist/client/index.js"
43
+ },
44
+ "./convex.config": {
45
+ "types": "./dist/component/convex.config.d.ts",
46
+ "default": "./dist/component/convex.config.js"
47
+ },
48
+ "./convex.config.js": {
49
+ "types": "./dist/component/convex.config.d.ts",
50
+ "default": "./dist/component/convex.config.js"
51
+ },
52
+ "./_generated/component.js": {
53
+ "types": "./dist/component/_generated/component.d.ts"
54
+ }
55
+ },
56
+ "types": "./dist/client/index.d.ts",
57
+ "module": "./dist/client/index.js",
58
+ "dependencies": {
59
+ "@convex-dev/sharded-counter": "^0.2.1"
60
+ },
61
+ "peerDependencies": {
62
+ "@convex-dev/ai-sdk-provider": "*",
63
+ "ai": "^5.0.0 || ^6.0.0 || ^7.0.0",
64
+ "convex": "^1.45.0"
65
+ },
66
+ "devDependencies": {
67
+ "@edge-runtime/vm": "^5.0.0",
68
+ "convex": "^1.45.0",
69
+ "convex-test": "^0.0.38",
70
+ "typescript": "^5.7.0",
71
+ "vitest": "^4.0.0"
72
+ }
73
+ }