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

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;
@@ -114,6 +150,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
114
150
  promptTokens?: number | undefined;
115
151
  completionTokens?: number | undefined;
116
152
  cachedTokens?: number | undefined;
153
+ serverToolUses?: Record<string, number> | undefined;
117
154
  costNanos?: number | undefined;
118
155
  latencyMs?: number | undefined;
119
156
  rerunOf?: import("convex/values").GenericId<"requests"> | undefined;
@@ -127,6 +164,16 @@ declare const _default: import("convex/server").SchemaDefinition<{
127
164
  }, {
128
165
  userId: import("convex/values").VString<string, "required">;
129
166
  actionName: import("convex/values").VString<string | undefined, "optional">;
167
+ tags: import("convex/values").VArray<{
168
+ dimension: string;
169
+ value: string;
170
+ }[] | undefined, import("convex/values").VObject<{
171
+ dimension: string;
172
+ value: string;
173
+ }, {
174
+ dimension: import("convex/values").VString<string, "required">;
175
+ value: import("convex/values").VString<string, "required">;
176
+ }, "required", "dimension" | "value">, "optional">;
130
177
  model: import("convex/values").VString<string, "required">;
131
178
  estimatedNanos: import("convex/values").VFloat64<number | undefined, "optional">;
132
179
  estimatedTokens: import("convex/values").VFloat64<number | undefined, "optional">;
@@ -149,10 +196,11 @@ declare const _default: import("convex/server").SchemaDefinition<{
149
196
  promptTokens: import("convex/values").VFloat64<number | undefined, "optional">;
150
197
  completionTokens: import("convex/values").VFloat64<number | undefined, "optional">;
151
198
  cachedTokens: import("convex/values").VFloat64<number | undefined, "optional">;
199
+ serverToolUses: import("convex/values").VRecord<Record<string, number> | undefined, import("convex/values").VString<string, "required">, import("convex/values").VFloat64<number, "required">, "optional", string>;
152
200
  costNanos: import("convex/values").VFloat64<number | undefined, "optional">;
153
201
  latencyMs: import("convex/values").VFloat64<number | undefined, "optional">;
154
202
  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">, {
203
+ }, "required", "userId" | "actionName" | "tags" | "model" | "estimatedNanos" | "estimatedTokens" | "unpricedModel" | "overBudget" | "settled" | "messages" | "status" | "error" | "responseText" | "promptTokens" | "completionTokens" | "cachedTokens" | "serverToolUses" | "costNanos" | "latencyMs" | "rerunOf" | `serverToolUses.${string}`>, {
156
204
  userId: ["userId", "_creationTime"];
157
205
  status: ["status", "_creationTime"];
158
206
  rerunOf: ["rerunOf", "_creationTime"];
@@ -160,6 +208,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
160
208
  settled: ["settled", "_creationTime"];
161
209
  }, {}, {}>;
162
210
  prices: import("convex/server").TableDefinition<import("convex/values").VObject<{
211
+ cachedNanosPerMTok?: number | undefined;
163
212
  model: string;
164
213
  inputNanosPerMTok: number;
165
214
  outputNanosPerMTok: number;
@@ -167,7 +216,8 @@ declare const _default: import("convex/server").SchemaDefinition<{
167
216
  model: import("convex/values").VString<string, "required">;
168
217
  inputNanosPerMTok: import("convex/values").VFloat64<number, "required">;
169
218
  outputNanosPerMTok: import("convex/values").VFloat64<number, "required">;
170
- }, "required", "model" | "inputNanosPerMTok" | "outputNanosPerMTok">, {
219
+ cachedNanosPerMTok: import("convex/values").VFloat64<number | undefined, "optional">;
220
+ }, "required", "model" | "inputNanosPerMTok" | "outputNanosPerMTok" | "cachedNanosPerMTok">, {
171
221
  model: ["model", "_creationTime"];
172
222
  }, {}, {}>;
173
223
  settings: import("convex/server").TableDefinition<import("convex/values").VObject<{
@@ -180,6 +230,8 @@ declare const _default: import("convex/server").SchemaDefinition<{
180
230
  globalLifetimeBumpNanos?: number | undefined;
181
231
  globalBumpDayStamp?: string | undefined;
182
232
  retentionMs?: number | undefined;
233
+ defaultWarnAtPct?: number | undefined;
234
+ serverToolPrices?: Record<string, number> | undefined;
183
235
  key: string;
184
236
  }, {
185
237
  key: import("convex/values").VString<string, "required">;
@@ -192,7 +244,9 @@ declare const _default: import("convex/server").SchemaDefinition<{
192
244
  globalLifetimeBumpNanos: import("convex/values").VFloat64<number | undefined, "optional">;
193
245
  globalBumpDayStamp: import("convex/values").VString<string | undefined, "optional">;
194
246
  retentionMs: import("convex/values").VFloat64<number | undefined, "optional">;
195
- }, "required", "key" | "modelMode" | "models" | "globalDailySpendLimitNanos" | "globalLifetimeSpendLimitNanos" | "globalEnforcement" | "globalDailyBumpNanos" | "globalLifetimeBumpNanos" | "globalBumpDayStamp" | "retentionMs">, {
247
+ defaultWarnAtPct: import("convex/values").VFloat64<number | undefined, "optional">;
248
+ serverToolPrices: import("convex/values").VRecord<Record<string, number> | undefined, import("convex/values").VString<string, "required">, import("convex/values").VFloat64<number, "required">, "optional", string>;
249
+ }, "required", "key" | "modelMode" | "models" | "globalDailySpendLimitNanos" | "globalLifetimeSpendLimitNanos" | "globalEnforcement" | "globalDailyBumpNanos" | "globalLifetimeBumpNanos" | "globalBumpDayStamp" | "retentionMs" | "defaultWarnAtPct" | "serverToolPrices" | `serverToolPrices.${string}`>, {
196
250
  key: ["key", "_creationTime"];
197
251
  }, {}, {}>;
198
252
  }, 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()), // rolling limit for this bucket
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()),
@@ -83,6 +120,9 @@ export default defineSchema({
83
120
  completionTokens: v.optional(v.number()),
84
121
  // subset of promptTokens served from the provider's prompt cache (cheaper).
85
122
  cachedTokens: v.optional(v.number()),
123
+ // server-side tool invocations that bill a per-call fee on top of tokens
124
+ // (e.g. { web_search: 3 }). Priced via serverToolPrices at settle.
125
+ serverToolUses: v.optional(v.record(v.string(), v.number())),
86
126
  costNanos: v.optional(v.number()),
87
127
  latencyMs: v.optional(v.number()),
88
128
  rerunOf: v.optional(v.id("requests")),
@@ -97,6 +137,9 @@ export default defineSchema({
97
137
  model: v.string(),
98
138
  inputNanosPerMTok: v.number(),
99
139
  outputNanosPerMTok: v.number(),
140
+ // price for cached (prompt-cache-read) input tokens. Providers bill these
141
+ // at a fraction of the input rate; if unset, a default discount is applied.
142
+ cachedNanosPerMTok: v.optional(v.number()),
100
143
  }).index("model", ["model"]),
101
144
  // singleton component config (key === "singleton")
102
145
  settings: defineTable({
@@ -105,12 +148,12 @@ export default defineSchema({
105
148
  // "denylist": any model except the listed ones.
106
149
  modelMode: v.optional(v.union(v.literal("open"), v.literal("allowlist"), v.literal("denylist"))),
107
150
  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.
151
+ // Deployment-wide ("global") spend cap across ALL requests. Running totals
152
+ // live in a sharded counter (high write throughput) since every request
153
+ // touches it; only the limit config lives here. Enforced approximately —
154
+ // the sharded total is read without a reservation, so under heavy
155
+ // concurrency it can overshoot by a bounded amount. Right for a global
156
+ // killswitch; per-bucket concurrent admission is atomic via reserve/settle.
114
157
  globalDailySpendLimitNanos: v.optional(v.number()),
115
158
  globalLifetimeSpendLimitNanos: v.optional(v.number()),
116
159
  globalEnforcement: v.optional(v.union(v.literal("hard"), v.literal("soft"))),
@@ -119,5 +162,11 @@ export default defineSchema({
119
162
  globalBumpDayStamp: v.optional(v.string()),
120
163
  // request-row retention window in ms (default 1h); 0 disables sweeping.
121
164
  retentionMs: v.optional(v.number()),
165
+ // default approaching-limit alert threshold (fraction of a cap) for buckets
166
+ // that don't set their own warnAtPct. 0/unset disables threshold alerts.
167
+ defaultWarnAtPct: v.optional(v.number()),
168
+ // per-call price (nanodollars) overrides for provider server tools, keyed by
169
+ // tool name (e.g. { web_search: 12_000_000 }). Merged over the defaults.
170
+ serverToolPrices: v.optional(v.record(v.string(), v.number())),
122
171
  }).index("key", ["key"]),
123
172
  });
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.0",
10
+ "version": "0.0.2-alpha.12",
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": "^5.0.0",
68
- "convex": "^1.45.0",
69
- "convex-test": "^0.0.38",
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": "^4.0.0"
71
+ "vitest": "4.1.11"
72
72
  }
73
73
  }