@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.
@@ -5,105 +5,141 @@ 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
- users: import("convex/server").TableDefinition<import("convex/values").VObject<{
16
+ buckets: import("convex/server").TableDefinition<import("convex/values").VObject<{
10
17
  requestsPerMinute?: number | undefined;
18
+ maxConcurrent?: number | undefined;
11
19
  dailySpendLimitNanos?: number | undefined;
20
+ monthlySpendLimitNanos?: number | undefined;
12
21
  lifetimeSpendLimitNanos?: number | undefined;
13
22
  dailyTokenLimit?: number | undefined;
23
+ monthlyTokenLimit?: number | undefined;
14
24
  lifetimeTokenLimit?: number | undefined;
15
25
  blocked?: boolean | undefined;
26
+ warnAtPct?: number | undefined;
16
27
  enforcement?: "hard" | "soft" | undefined;
17
28
  dailyBumpNanos?: number | undefined;
29
+ monthlyBumpNanos?: number | undefined;
18
30
  lifetimeBumpNanos?: number | undefined;
19
31
  bumpDayStamp?: string | undefined;
32
+ bumpMonthStamp?: string | undefined;
20
33
  tokensToday?: number | undefined;
34
+ monthStamp?: string | undefined;
35
+ spendThisMonthNanos?: number | undefined;
36
+ tokensThisMonth?: number | undefined;
21
37
  reservedTodayNanos?: number | undefined;
38
+ reservedMonthNanos?: number | undefined;
22
39
  reservedTotalNanos?: number | undefined;
23
40
  reservedTodayTokens?: number | undefined;
41
+ reservedMonthTokens?: number | undefined;
24
42
  reservedTotalTokens?: number | undefined;
25
43
  pendingCount?: number | undefined;
26
- userId: string;
44
+ dimension: string;
45
+ value: string;
27
46
  totalSpendNanos: number;
28
47
  totalRequests: number;
29
48
  totalTokens: number;
30
49
  dayStamp: string;
31
50
  spendTodayNanos: number;
32
51
  }, {
33
- userId: import("convex/values").VString<string, "required">;
52
+ dimension: import("convex/values").VString<string, "required">;
53
+ value: import("convex/values").VString<string, "required">;
34
54
  requestsPerMinute: import("convex/values").VFloat64<number | undefined, "optional">;
55
+ maxConcurrent: import("convex/values").VFloat64<number | undefined, "optional">;
35
56
  dailySpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
57
+ monthlySpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
36
58
  lifetimeSpendLimitNanos: import("convex/values").VFloat64<number | undefined, "optional">;
37
59
  dailyTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
60
+ monthlyTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
38
61
  lifetimeTokenLimit: import("convex/values").VFloat64<number | undefined, "optional">;
39
62
  blocked: import("convex/values").VBoolean<boolean | undefined, "optional">;
63
+ warnAtPct: import("convex/values").VFloat64<number | undefined, "optional">;
40
64
  enforcement: import("convex/values").VUnion<"hard" | "soft" | undefined, [import("convex/values").VLiteral<"hard", "required">, import("convex/values").VLiteral<"soft", "required">], "optional", never>;
41
65
  dailyBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
66
+ monthlyBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
42
67
  lifetimeBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
43
68
  bumpDayStamp: import("convex/values").VString<string | undefined, "optional">;
69
+ bumpMonthStamp: import("convex/values").VString<string | undefined, "optional">;
44
70
  totalSpendNanos: import("convex/values").VFloat64<number, "required">;
45
71
  totalRequests: import("convex/values").VFloat64<number, "required">;
46
72
  totalTokens: import("convex/values").VFloat64<number, "required">;
47
73
  dayStamp: import("convex/values").VString<string, "required">;
48
74
  spendTodayNanos: import("convex/values").VFloat64<number, "required">;
49
75
  tokensToday: import("convex/values").VFloat64<number | undefined, "optional">;
76
+ monthStamp: import("convex/values").VString<string | undefined, "optional">;
77
+ spendThisMonthNanos: import("convex/values").VFloat64<number | undefined, "optional">;
78
+ tokensThisMonth: import("convex/values").VFloat64<number | undefined, "optional">;
50
79
  reservedTodayNanos: import("convex/values").VFloat64<number | undefined, "optional">;
80
+ reservedMonthNanos: import("convex/values").VFloat64<number | undefined, "optional">;
51
81
  reservedTotalNanos: import("convex/values").VFloat64<number | undefined, "optional">;
52
82
  reservedTodayTokens: import("convex/values").VFloat64<number | undefined, "optional">;
83
+ reservedMonthTokens: import("convex/values").VFloat64<number | undefined, "optional">;
53
84
  reservedTotalTokens: import("convex/values").VFloat64<number | undefined, "optional">;
54
85
  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"];
86
+ }, "required", "dimension" | "value" | "requestsPerMinute" | "maxConcurrent" | "dailySpendLimitNanos" | "monthlySpendLimitNanos" | "lifetimeSpendLimitNanos" | "dailyTokenLimit" | "monthlyTokenLimit" | "lifetimeTokenLimit" | "blocked" | "warnAtPct" | "enforcement" | "dailyBumpNanos" | "monthlyBumpNanos" | "lifetimeBumpNanos" | "bumpDayStamp" | "bumpMonthStamp" | "totalSpendNanos" | "totalRequests" | "totalTokens" | "dayStamp" | "spendTodayNanos" | "tokensToday" | "monthStamp" | "spendThisMonthNanos" | "tokensThisMonth" | "reservedTodayNanos" | "reservedMonthNanos" | "reservedTotalNanos" | "reservedTodayTokens" | "reservedMonthTokens" | "reservedTotalTokens" | "pendingCount">, {
87
+ dim_value: ["dimension", "value", "_creationTime"];
88
+ dimension: ["dimension", "_creationTime"];
57
89
  }, {}, {}>;
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;
90
+ usage: import("convex/server").TableDefinition<import("convex/values").VObject<{
91
+ dimension: string;
92
+ value: string;
93
+ period: "day" | "month";
94
+ stamp: string;
95
+ spendNanos: number;
96
+ tokens: number;
97
+ requests: number;
80
98
  }, {
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"];
99
+ dimension: import("convex/values").VString<string, "required">;
100
+ value: import("convex/values").VString<string, "required">;
101
+ period: import("convex/values").VUnion<"day" | "month", [import("convex/values").VLiteral<"day", "required">, import("convex/values").VLiteral<"month", "required">], "required", never>;
102
+ stamp: import("convex/values").VString<string, "required">;
103
+ spendNanos: import("convex/values").VFloat64<number, "required">;
104
+ tokens: import("convex/values").VFloat64<number, "required">;
105
+ requests: import("convex/values").VFloat64<number, "required">;
106
+ }, "required", "dimension" | "value" | "period" | "stamp" | "spendNanos" | "tokens" | "requests">, {
107
+ bucket_period_stamp: ["dimension", "value", "period", "stamp", "_creationTime"];
108
+ period_stamp: ["period", "stamp", "_creationTime"];
109
+ }, {}, {}>;
110
+ requestTags: import("convex/server").TableDefinition<import("convex/values").VObject<{
111
+ dimension: string;
112
+ value: string;
113
+ requestId: import("convex/values").GenericId<"requests">;
114
+ }, {
115
+ dimension: import("convex/values").VString<string, "required">;
116
+ value: import("convex/values").VString<string, "required">;
117
+ requestId: import("convex/values").VId<import("convex/values").GenericId<"requests">, "required">;
118
+ }, "required", "dimension" | "value" | "requestId">, {
119
+ dim_value: ["dimension", "value", "_creationTime"];
120
+ requestId: ["requestId", "_creationTime"];
121
+ }, {}, {}>;
122
+ adjustments: import("convex/server").TableDefinition<import("convex/values").VObject<{
123
+ tokens?: number | undefined;
124
+ reason?: string | undefined;
125
+ dimension: string;
126
+ value: string;
127
+ deltaNanos: number;
128
+ }, {
129
+ dimension: import("convex/values").VString<string, "required">;
130
+ value: import("convex/values").VString<string, "required">;
131
+ deltaNanos: import("convex/values").VFloat64<number, "required">;
132
+ tokens: import("convex/values").VFloat64<number | undefined, "optional">;
133
+ reason: import("convex/values").VString<string | undefined, "optional">;
134
+ }, "required", "dimension" | "value" | "tokens" | "deltaNanos" | "reason">, {
135
+ dim_value: ["dimension", "value", "_creationTime"];
104
136
  }, {}, {}>;
105
137
  requests: import("convex/server").TableDefinition<import("convex/values").VObject<{
106
138
  actionName?: string | undefined;
139
+ tags?: {
140
+ dimension: string;
141
+ value: string;
142
+ }[] | undefined;
107
143
  estimatedNanos?: number | undefined;
108
144
  estimatedTokens?: number | undefined;
109
145
  unpricedModel?: boolean | undefined;
@@ -127,6 +163,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
127
163
  }, {
128
164
  userId: import("convex/values").VString<string, "required">;
129
165
  actionName: import("convex/values").VString<string | undefined, "optional">;
166
+ tags: import("convex/values").VArray<{
167
+ dimension: string;
168
+ value: string;
169
+ }[] | undefined, import("convex/values").VObject<{
170
+ dimension: string;
171
+ value: string;
172
+ }, {
173
+ dimension: import("convex/values").VString<string, "required">;
174
+ value: import("convex/values").VString<string, "required">;
175
+ }, "required", "dimension" | "value">, "optional">;
130
176
  model: import("convex/values").VString<string, "required">;
131
177
  estimatedNanos: import("convex/values").VFloat64<number | undefined, "optional">;
132
178
  estimatedTokens: import("convex/values").VFloat64<number | undefined, "optional">;
@@ -152,7 +198,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
152
198
  costNanos: import("convex/values").VFloat64<number | undefined, "optional">;
153
199
  latencyMs: import("convex/values").VFloat64<number | undefined, "optional">;
154
200
  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">, {
201
+ }, "required", "userId" | "actionName" | "tags" | "model" | "estimatedNanos" | "estimatedTokens" | "unpricedModel" | "overBudget" | "settled" | "messages" | "status" | "error" | "responseText" | "promptTokens" | "completionTokens" | "cachedTokens" | "costNanos" | "latencyMs" | "rerunOf">, {
156
202
  userId: ["userId", "_creationTime"];
157
203
  status: ["status", "_creationTime"];
158
204
  rerunOf: ["rerunOf", "_creationTime"];
@@ -160,6 +206,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
160
206
  settled: ["settled", "_creationTime"];
161
207
  }, {}, {}>;
162
208
  prices: import("convex/server").TableDefinition<import("convex/values").VObject<{
209
+ cachedNanosPerMTok?: number | undefined;
163
210
  model: string;
164
211
  inputNanosPerMTok: number;
165
212
  outputNanosPerMTok: number;
@@ -167,7 +214,8 @@ declare const _default: import("convex/server").SchemaDefinition<{
167
214
  model: import("convex/values").VString<string, "required">;
168
215
  inputNanosPerMTok: import("convex/values").VFloat64<number, "required">;
169
216
  outputNanosPerMTok: import("convex/values").VFloat64<number, "required">;
170
- }, "required", "model" | "inputNanosPerMTok" | "outputNanosPerMTok">, {
217
+ cachedNanosPerMTok: import("convex/values").VFloat64<number | undefined, "optional">;
218
+ }, "required", "model" | "inputNanosPerMTok" | "outputNanosPerMTok" | "cachedNanosPerMTok">, {
171
219
  model: ["model", "_creationTime"];
172
220
  }, {}, {}>;
173
221
  settings: import("convex/server").TableDefinition<import("convex/values").VObject<{
@@ -180,6 +228,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
180
228
  globalLifetimeBumpNanos?: number | undefined;
181
229
  globalBumpDayStamp?: string | undefined;
182
230
  retentionMs?: number | undefined;
231
+ defaultWarnAtPct?: number | undefined;
183
232
  key: string;
184
233
  }, {
185
234
  key: import("convex/values").VString<string, "required">;
@@ -192,7 +241,8 @@ declare const _default: import("convex/server").SchemaDefinition<{
192
241
  globalLifetimeBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
193
242
  globalBumpDayStamp: import("convex/values").VString<string | undefined, "optional">;
194
243
  retentionMs: import("convex/values").VFloat64<number | undefined, "optional">;
195
- }, "required", "key" | "modelMode" | "models" | "globalDailySpendLimitNanos" | "globalLifetimeSpendLimitNanos" | "globalEnforcement" | "globalDailyBumpNanos" | "globalLifetimeBumpNanos" | "globalBumpDayStamp" | "retentionMs">, {
244
+ defaultWarnAtPct: import("convex/values").VFloat64<number | undefined, "optional">;
245
+ }, "required", "key" | "modelMode" | "models" | "globalDailySpendLimitNanos" | "globalLifetimeSpendLimitNanos" | "globalEnforcement" | "globalDailyBumpNanos" | "globalLifetimeBumpNanos" | "globalBumpDayStamp" | "retentionMs" | "defaultWarnAtPct">, {
196
246
  key: ["key", "_creationTime"];
197
247
  }, {}, {}>;
198
248
  }, true>;
@@ -4,65 +4,102 @@ 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
- users: defineTable({
9
- userId: v.string(), // app-provided stable key (your user id)
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
20
+ maxConcurrent: v.optional(v.number()), // max in-flight (pending) requests
12
21
  dailySpendLimitNanos: v.optional(v.number()),
22
+ monthlySpendLimitNanos: v.optional(v.number()),
13
23
  lifetimeSpendLimitNanos: v.optional(v.number()),
14
24
  dailyTokenLimit: v.optional(v.number()),
25
+ monthlyTokenLimit: v.optional(v.number()),
15
26
  lifetimeTokenLimit: v.optional(v.number()),
16
- blocked: v.optional(v.boolean()),
27
+ blocked: v.optional(v.boolean()), // hard block (was `blocked`/`disabled`)
28
+ // Fire an approaching-limit alert once usage crosses this fraction of a cap
29
+ // (e.g. 0.8 = warn at 80%). Falls back to the deployment default.
30
+ warnAtPct: v.optional(v.number()),
17
31
  // "hard" (default): exceeding a budget blocks. "soft": warn but allow.
18
32
  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.
33
+ // one-time bumps ("approve another $X"). Daily/monthly bumps are scoped to
34
+ // their stamp (reset with the window); lifetime bump is permanent.
21
35
  dailyBumpNanos: v.optional(v.number()),
36
+ monthlyBumpNanos: v.optional(v.number()),
22
37
  lifetimeBumpNanos: v.optional(v.number()),
23
38
  bumpDayStamp: v.optional(v.string()),
39
+ bumpMonthStamp: v.optional(v.string()),
24
40
  // settled totals (from finished requests)
25
41
  totalSpendNanos: v.number(),
26
42
  totalRequests: v.number(),
27
43
  totalTokens: v.number(),
28
44
  // 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
45
  dayStamp: v.string(),
55
46
  spendTodayNanos: v.number(),
56
47
  tokensToday: v.optional(v.number()),
48
+ // monthly window (UTC calendar month, e.g. "2026-09")
49
+ monthStamp: v.optional(v.string()),
50
+ spendThisMonthNanos: v.optional(v.number()),
51
+ tokensThisMonth: v.optional(v.number()),
52
+ // in-flight reservations (pessimistic holds; released on settle/expiry)
57
53
  reservedTodayNanos: v.optional(v.number()),
54
+ reservedMonthNanos: v.optional(v.number()),
58
55
  reservedTotalNanos: v.optional(v.number()),
59
56
  reservedTodayTokens: v.optional(v.number()),
57
+ reservedMonthTokens: v.optional(v.number()),
60
58
  reservedTotalTokens: v.optional(v.number()),
61
59
  pendingCount: v.optional(v.number()),
62
- }).index("name", ["name"]),
60
+ })
61
+ .index("dim_value", ["dimension", "value"])
62
+ .index("dimension", ["dimension"]),
63
+ // Durable per-(bucket, period) spend history. Written from settled requests
64
+ // and manual adjustments; NEVER swept by request retention, so spend charts
65
+ // and "what did we spend last month" survive long after the raw request rows
66
+ // are purged. period is "day" ("2026-09-04") or "month" ("2026-09").
67
+ usage: defineTable({
68
+ dimension: v.string(),
69
+ value: v.string(),
70
+ period: v.union(v.literal("day"), v.literal("month")),
71
+ stamp: v.string(),
72
+ spendNanos: v.number(),
73
+ tokens: v.number(),
74
+ requests: v.number(),
75
+ })
76
+ .index("bucket_period_stamp", ["dimension", "value", "period", "stamp"])
77
+ .index("period_stamp", ["period", "stamp"]),
78
+ // Reverse index for filtering the request log by an arbitrary tag dimension
79
+ // (user/action are already indexed on `requests`). One row per extra tag per
80
+ // request; cleaned up with the request on retention/deletion.
81
+ requestTags: defineTable({
82
+ dimension: v.string(),
83
+ value: v.string(),
84
+ requestId: v.id("requests"),
85
+ })
86
+ .index("dim_value", ["dimension", "value"])
87
+ .index("requestId", ["requestId"]),
88
+ // Manual credits/debits applied to a bucket (comp a user, correct an
89
+ // overcharge). Negative delta = credit/refund, positive = extra charge.
90
+ adjustments: defineTable({
91
+ dimension: v.string(),
92
+ value: v.string(),
93
+ deltaNanos: v.number(),
94
+ tokens: v.optional(v.number()),
95
+ reason: v.optional(v.string()),
96
+ }).index("dim_value", ["dimension", "value"]),
63
97
  requests: defineTable({
98
+ // `user` and `action` stay first-class + indexed (the hot-path filters and
99
+ // rate limiting); the full attribution incl. extra tags lives in `tags`.
64
100
  userId: v.string(),
65
101
  actionName: v.optional(v.string()),
102
+ tags: v.optional(v.array(vTag)),
66
103
  model: v.string(),
67
104
  // pessimistic holds placed at start; reconciled to actual on settle
68
105
  estimatedNanos: v.optional(v.number()),
@@ -97,6 +134,9 @@ export default defineSchema({
97
134
  model: v.string(),
98
135
  inputNanosPerMTok: v.number(),
99
136
  outputNanosPerMTok: v.number(),
137
+ // price for cached (prompt-cache-read) input tokens. Providers bill these
138
+ // at a fraction of the input rate; if unset, a default discount is applied.
139
+ cachedNanosPerMTok: v.optional(v.number()),
100
140
  }).index("model", ["model"]),
101
141
  // singleton component config (key === "singleton")
102
142
  settings: defineTable({
@@ -105,12 +145,12 @@ export default defineSchema({
105
145
  // "denylist": any model except the listed ones.
106
146
  modelMode: v.optional(v.union(v.literal("open"), v.literal("allowlist"), v.literal("denylist"))),
107
147
  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.
148
+ // Deployment-wide ("global") spend cap across ALL requests. Running totals
149
+ // live in a sharded counter (high write throughput) since every request
150
+ // touches it; only the limit config lives here. Enforced approximately —
151
+ // the sharded total is read without a reservation, so under heavy
152
+ // concurrency it can overshoot by a bounded amount. Right for a global
153
+ // killswitch; per-bucket caps stay exact via reserve/settle.
114
154
  globalDailySpendLimitNanos: v.optional(v.number()),
115
155
  globalLifetimeSpendLimitNanos: v.optional(v.number()),
116
156
  globalEnforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
@@ -119,5 +159,8 @@ export default defineSchema({
119
159
  globalBumpDayStamp: v.optional(v.string()),
120
160
  // request-row retention window in ms (default 1h); 0 disables sweeping.
121
161
  retentionMs: v.optional(v.number()),
162
+ // default approaching-limit alert threshold (fraction of a cap) for buckets
163
+ // that don't set their own warnAtPct. 0/unset disables threshold alerts.
164
+ defaultWarnAtPct: v.optional(v.number()),
122
165
  }).index("key", ["key"]),
123
166
  });
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.3",
10
+ "version": "0.0.2-alpha.5",
11
11
  "license": "Apache-2.0",
12
12
  "type": "module",
13
13
  "keywords": [