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

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.
@@ -1,6 +1,8 @@
1
+ import { httpActionGeneric, } from "convex/server";
1
2
  import { ConvexError } from "convex/values";
2
3
  import { generateText, wrapLanguageModel } from "ai";
3
4
  import { convexGateway } from "@convex-dev/ai-sdk-provider";
5
+ import { DASHBOARD_HTML } from "./dashboard";
4
6
  // The calling Convex action's name (e.g. "ai:sendMessage"), unless overridden.
5
7
  async function resolveActionName(ctx, explicit) {
6
8
  if (explicit !== undefined)
@@ -39,14 +41,35 @@ function extractUsage(usage) {
39
41
  return {
40
42
  promptTokens: toTokenCount(usage?.inputTokens ?? usage?.promptTokens),
41
43
  completionTokens: toTokenCount(usage?.outputTokens ?? usage?.completionTokens),
42
- // cached prompt tokens: AI SDK v5 `cachedInputTokens`, OpenAI-compat
43
- // `prompt_tokens_details.cached_tokens` / `cached_tokens`.
44
- cachedTokens: toTokenCount(usage?.cachedInputTokens ??
44
+ // cached prompt tokens. The Convex gateway reports these at
45
+ // `usage.inputTokenDetails.cacheReadTokens`; the other paths cover AI SDK v5
46
+ // (`cachedInputTokens`) and raw OpenAI-compatible shapes.
47
+ cachedTokens: toTokenCount(usage?.inputTokenDetails?.cacheReadTokens ??
48
+ usage?.cachedInputTokens ??
45
49
  usage?.promptTokensDetails?.cachedTokens ??
46
50
  usage?.prompt_tokens_details?.cached_tokens ??
47
51
  usage?.cached_tokens),
48
52
  };
49
53
  }
54
+ // The AI Gateway reports the authoritative dollar cost of each request.
55
+ // @convex-dev/ai-sdk-provider surfaces it at
56
+ // `providerMetadata.convexGateway.cost` (USD); convert to nanodollars and pass
57
+ // it through as authoritative (finishRequest prefers it over the token-based
58
+ // estimate). No-op on older provider versions that don't surface it.
59
+ function extractGatewayCostNanos(result) {
60
+ const meta = result?.providerMetadata?.convexGateway;
61
+ const costUsd = meta?.cost;
62
+ if (typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd >= 0) {
63
+ return Math.round(costUsd * NANOS_PER_DOLLAR);
64
+ }
65
+ // Back-compat: honor an explicitly nano-denominated field if ever present.
66
+ const costNanos = meta?.costNanos ?? result?.usage?.costNanos;
67
+ if (typeof costNanos === "number" && Number.isFinite(costNanos) && costNanos >= 0) {
68
+ return Math.round(costNanos);
69
+ }
70
+ return undefined;
71
+ }
72
+ const NANOS_PER_DOLLAR = 1e9;
50
73
  // Flatten an AI SDK prompt (roles + content parts) into simple storable messages.
51
74
  function simplifyPrompt(prompt) {
52
75
  if (!Array.isArray(prompt))
@@ -79,23 +102,56 @@ function extractText(result) {
79
102
  return "";
80
103
  }
81
104
  // ---------- client ----------
105
+ // Length-independent-branch string compare, so the dashboard token check
106
+ // doesn't leak the token via response timing. (Length itself is not secret.)
107
+ function timingSafeEqual(a, b) {
108
+ if (a.length !== b.length)
109
+ return false;
110
+ let r = 0;
111
+ for (let i = 0; i < a.length; i++)
112
+ r |= a.charCodeAt(i) ^ b.charCodeAt(i);
113
+ return r === 0;
114
+ }
82
115
  export class AIBudget {
83
116
  component;
84
117
  defaultModel;
85
118
  onSoftLimit;
119
+ onThreshold;
120
+ onLimitReached;
86
121
  constructor(component, options) {
87
122
  this.component = component;
88
123
  this.defaultModel = options?.defaultModel ?? "openai/gpt-4o-mini";
89
124
  this.onSoftLimit = options?.onSoftLimit;
125
+ this.onThreshold = options?.onThreshold;
126
+ this.onLimitReached = options?.onLimitReached;
127
+ }
128
+ // Fire the soft-limit + threshold callbacks from a startRequest result.
129
+ async fireBudgetEvents(base, warnings, notices) {
130
+ if (warnings.length > 0 && this.onSoftLimit) {
131
+ try {
132
+ await this.onSoftLimit({ ...base, messages: warnings, warnings });
133
+ }
134
+ catch {
135
+ /* never let a callback error break a request */
136
+ }
137
+ }
138
+ if (notices.length > 0 && this.onThreshold) {
139
+ try {
140
+ await this.onThreshold({ ...base, messages: notices });
141
+ }
142
+ catch {
143
+ /* swallow */
144
+ }
145
+ }
90
146
  }
91
- async fireSoftLimit(info) {
92
- if (info.warnings.length === 0 || !this.onSoftLimit)
147
+ async fireLimitReached(info) {
148
+ if (!this.onLimitReached)
93
149
  return;
94
150
  try {
95
- await this.onSoftLimit(info);
151
+ await this.onLimitReached(info);
96
152
  }
97
153
  catch {
98
- // never let a callback error break a request
154
+ /* swallow */
99
155
  }
100
156
  }
101
157
  /**
@@ -110,11 +166,20 @@ export class AIBudget {
110
166
  const started = await ctx.runMutation(this.component.lib.startRequest, {
111
167
  userId,
112
168
  actionName,
169
+ tags: args.tags,
113
170
  model,
114
171
  messages,
115
172
  rerunOf: args.rerunOf,
116
173
  });
117
174
  if (!started.allowed) {
175
+ await this.fireLimitReached({
176
+ userId,
177
+ action: actionName,
178
+ tags: args.tags,
179
+ messages: [started.reason],
180
+ code: started.code,
181
+ reason: started.reason,
182
+ });
118
183
  throw new ConvexError({
119
184
  kind: "AIBudgetLimit",
120
185
  code: started.code,
@@ -123,7 +188,8 @@ export class AIBudget {
123
188
  }
124
189
  const requestId = started.requestId;
125
190
  const warnings = started.warnings;
126
- await this.fireSoftLimit({ userId, action: actionName, requestId, warnings });
191
+ const notices = started.notices;
192
+ await this.fireBudgetEvents({ userId, action: actionName, tags: args.tags, requestId }, warnings, notices);
127
193
  const start = Date.now();
128
194
  try {
129
195
  // The full chain (incl. system) is stored on the request for audit/replay,
@@ -143,9 +209,10 @@ export class AIBudget {
143
209
  requestId,
144
210
  responseText: result.text,
145
211
  ...usage,
212
+ costNanos: extractGatewayCostNanos(result),
146
213
  latencyMs: Date.now() - start,
147
214
  });
148
- return { text: result.text, requestId, costNanos, warnings, ...usage };
215
+ return { text: result.text, requestId, costNanos, warnings, notices, ...usage };
149
216
  }
150
217
  catch (e) {
151
218
  await ctx.runMutation(this.component.lib.finishRequest, {
@@ -165,29 +232,33 @@ export class AIBudget {
165
232
  languageModel(ctx, opts = {}) {
166
233
  const modelId = opts.model ?? this.defaultModel;
167
234
  const component = this.component;
168
- const fireSoftLimit = this.fireSoftLimit.bind(this);
235
+ const fireBudgetEvents = this.fireBudgetEvents.bind(this);
236
+ const fireLimitReached = this.fireLimitReached.bind(this);
169
237
  const begin = async (params) => {
170
238
  const userId = await resolveUserId(ctx, opts.userId);
171
239
  const actionName = await resolveActionName(ctx, opts.action);
240
+ const base = { userId, action: actionName, tags: opts.tags };
172
241
  const started = await ctx.runMutation(component.lib.startRequest, {
173
242
  userId,
174
243
  actionName,
244
+ tags: opts.tags,
175
245
  model: modelId,
176
246
  messages: simplifyPrompt(params.prompt),
177
247
  });
178
248
  if (!started.allowed) {
249
+ await fireLimitReached({
250
+ ...base,
251
+ messages: [started.reason],
252
+ code: started.code,
253
+ reason: started.reason,
254
+ });
179
255
  throw new ConvexError({
180
256
  kind: "AIBudgetLimit",
181
257
  code: started.code,
182
258
  reason: started.reason,
183
259
  });
184
260
  }
185
- await fireSoftLimit({
186
- userId,
187
- action: actionName,
188
- requestId: started.requestId,
189
- warnings: started.warnings,
190
- });
261
+ await fireBudgetEvents({ ...base, requestId: started.requestId }, started.warnings, started.notices);
191
262
  return started.requestId;
192
263
  };
193
264
  const finish = async (requestId, fields) => ctx.runMutation(component.lib.finishRequest, { requestId, ...fields });
@@ -202,6 +273,7 @@ export class AIBudget {
202
273
  await finish(requestId, {
203
274
  responseText: extractText(result),
204
275
  ...extractUsage(result.usage),
276
+ costNanos: extractGatewayCostNanos(result),
205
277
  latencyMs: Date.now() - start,
206
278
  });
207
279
  return result;
@@ -219,6 +291,7 @@ export class AIBudget {
219
291
  const start = Date.now();
220
292
  let text = "";
221
293
  let usage = undefined;
294
+ let providerMetadata = undefined;
222
295
  try {
223
296
  const result = await doStream();
224
297
  // finishRequest is idempotent (terminal-guarded server-side), so
@@ -235,6 +308,7 @@ export class AIBudget {
235
308
  responseText: text,
236
309
  error,
237
310
  ...extractUsage(usage),
311
+ costNanos: extractGatewayCostNanos({ providerMetadata }),
238
312
  latencyMs: Date.now() - start,
239
313
  });
240
314
  };
@@ -243,8 +317,10 @@ export class AIBudget {
243
317
  if (chunk?.type === "text-delta") {
244
318
  text += chunk.delta ?? chunk.textDelta ?? "";
245
319
  }
246
- if (chunk?.type === "finish")
320
+ if (chunk?.type === "finish") {
247
321
  usage = chunk.usage;
322
+ providerMetadata = chunk.providerMetadata ?? providerMetadata;
323
+ }
248
324
  if (chunk?.type === "error")
249
325
  void settle(String(chunk.error));
250
326
  controller.enqueue(chunk);
@@ -278,6 +354,7 @@ export class AIBudget {
278
354
  messages: args.messages ?? original.messages,
279
355
  rerunOf: args.requestId,
280
356
  action: original.actionName,
357
+ tags: original.tags,
281
358
  });
282
359
  }
283
360
  // ---------- namespaced admin API ----------
@@ -285,35 +362,88 @@ export class AIBudget {
285
362
  get requests() {
286
363
  const c = this.component;
287
364
  return {
365
+ /** Filter by userId, or by any {dimension, value} (incl. custom tags). */
288
366
  list: (ctx, args = {}) => ctx.runQuery(c.lib.listRequests, args),
367
+ /** One request, including its stored prompt and response. */
368
+ get: (ctx, args) => ctx.runQuery(c.lib.getRequest, { requestId: args.requestId }),
289
369
  /** Ancestors up to the original, plus direct re-runs. */
290
370
  lineage: (ctx, args) => ctx.runQuery(c.lib.lineage, { requestId: args.requestId }),
291
371
  /** Replay a stored request, optionally with edited messages/model. */
292
372
  rerun: (ctx, args) => this.rerunImpl(ctx, args),
293
373
  };
294
374
  }
295
- /** Per-user budgets and controls. */
296
- get users() {
375
+ /**
376
+ * Budgets and controls for an arbitrary attribution dimension — the
377
+ * generalization of `users`/`actions`. Give it any dimension name (team,
378
+ * project, tenant, customer, env, feature, …) and set caps per value:
379
+ *
380
+ * ai.tag("customer").setLimits(ctx, { value: "acme", monthlySpendLimitNanos });
381
+ * ai.tag("customer").history(ctx, { value: "acme", period: "day" });
382
+ *
383
+ * Attribute a call to it by passing `tags` to `chat`/`languageModel`.
384
+ */
385
+ tag(dimension) {
386
+ return this.dimensionApi(dimension, (a) => a.value);
387
+ }
388
+ // Shared implementation behind tag()/users/actions. `key` maps the namespace's
389
+ // id field (value/userId/name) to the bucket value.
390
+ dimensionApi(dimension, key) {
297
391
  const c = this.component;
298
392
  return {
299
- list: (ctx) => ctx.runQuery(c.lib.listUsers, {}),
300
- setLimits: (ctx, args) => ctx.runMutation(c.lib.setLimits, args),
301
- /** One-time "approve another $X" bump (daily is today-only). */
302
- bump: (ctx, args) => ctx.runMutation(c.lib.bumpUser, args),
303
- /** Delete a user and all their request rows. */
304
- delete: (ctx, args) => ctx.runMutation(c.lib.deleteUser, args),
393
+ /** All buckets in this dimension. */
394
+ list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension }),
395
+ /** One bucket's limits + spend (null if it has none yet). */
396
+ get: (ctx, args) => ctx.runQuery(c.lib.getBucket, { dimension, value: key(args) }),
397
+ setLimits: (ctx, args) => {
398
+ const { value, userId, name, ...limits } = args;
399
+ return ctx.runMutation(c.lib.setBucketLimits, {
400
+ dimension,
401
+ value: key(args),
402
+ ...limits,
403
+ });
404
+ },
405
+ /** One-time "approve another $X" bump (daily/monthly reset with the window). */
406
+ bump: (ctx, args) => ctx.runMutation(c.lib.bumpBucket, {
407
+ dimension,
408
+ value: key(args),
409
+ dailyNanos: args.dailyNanos,
410
+ monthlyNanos: args.monthlyNanos,
411
+ lifetimeNanos: args.lifetimeNanos,
412
+ }),
413
+ /** Manually credit (negative) or debit (positive) this bucket. */
414
+ adjust: (ctx, args) => ctx.runMutation(c.lib.adjustBucket, {
415
+ dimension,
416
+ value: key(args),
417
+ deltaNanos: args.deltaNanos,
418
+ tokens: args.tokens,
419
+ reason: args.reason,
420
+ }),
421
+ /** Durable spend history for this bucket (per day or per month). */
422
+ history: (ctx, args) => ctx.runQuery(c.lib.usageHistory, {
423
+ dimension,
424
+ value: key(args),
425
+ period: args.period ?? "day",
426
+ limit: args.limit,
427
+ }),
428
+ /** Manual-adjustment audit log for this bucket. */
429
+ adjustments: (ctx, args) => ctx.runQuery(c.lib.listAdjustments, {
430
+ dimension,
431
+ value: key(args),
432
+ limit: args.limit,
433
+ }),
434
+ /** Delete the bucket (for "user", also its request rows). */
435
+ delete: (ctx, args) => ctx.runMutation(c.lib.deleteBucket, { dimension, value: key(args) }),
305
436
  };
306
437
  }
307
- /** Per-action (per-feature) budgets. */
438
+ /** Per-user budgets and controls — sugar over the "user" dimension. */
439
+ get users() {
440
+ return this.dimensionApi("user", (a) => a.userId);
441
+ }
442
+ /** Per-action (per-feature) budgets — sugar over the "action" dimension. */
308
443
  get actions() {
309
- const c = this.component;
310
- return {
311
- list: (ctx) => ctx.runQuery(c.lib.listActions, {}),
312
- setLimits: (ctx, args) => ctx.runMutation(c.lib.setActionLimits, args),
313
- bump: (ctx, args) => ctx.runMutation(c.lib.bumpAction, args),
314
- };
444
+ return this.dimensionApi("action", (a) => a.name);
315
445
  }
316
- /** The deployment-wide budget and retention config. */
446
+ /** The deployment-wide budget, alerts, and retention config. */
317
447
  get global() {
318
448
  const c = this.component;
319
449
  return {
@@ -322,6 +452,8 @@ export class AIBudget {
322
452
  /** A killswitch spend cap across all users/actions (enforced approximately). */
323
453
  setLimits: (ctx, args) => ctx.runMutation(c.lib.setGlobalLimits, args),
324
454
  bump: (ctx, args) => ctx.runMutation(c.lib.bumpGlobal, args),
455
+ /** Default approaching-limit alert threshold (fraction of a cap, e.g. 0.8). */
456
+ setAlertDefaults: (ctx, args) => ctx.runMutation(c.lib.setAlertDefaults, args),
325
457
  /** Request-row retention window in ms (default 1h; 0 disables). */
326
458
  setRetention: (ctx, args) => ctx.runMutation(c.lib.setRetention, args),
327
459
  };
@@ -343,6 +475,117 @@ export class AIBudget {
343
475
  set: (ctx, args) => ctx.runMutation(c.lib.setPrice, args),
344
476
  };
345
477
  }
478
+ /**
479
+ * Mount the built-in admin dashboard on your app's HTTP router with one call.
480
+ * Serves a self-contained HTML dashboard (buckets, requests, usage history,
481
+ * settings) plus a small JSON API, all backed by the component — no extra
482
+ * queries to write.
483
+ *
484
+ * // convex/http.ts
485
+ * import { httpRouter } from "convex/server";
486
+ * const http = httpRouter();
487
+ * ai.registerRoutes(http, { authorize: async (ctx) =>
488
+ * (await ctx.auth.getUserIdentity())?.role === "admin" });
489
+ * export default http;
490
+ *
491
+ * It then lives at `https://<deployment>.convex.site/aibudget`.
492
+ *
493
+ * SECURITY: the endpoint is public on the internet. You MUST gate it — either
494
+ * pass `authorize` (recommended: check the caller is a deployment admin) or
495
+ * set the `AI_BUDGET_DASHBOARD_TOKEN` env var (a bearer token / `?token=`).
496
+ * With neither, every route returns 401.
497
+ */
498
+ registerRoutes(http, opts = {}) {
499
+ const prefix = (opts.path ?? "/aibudget").replace(/\/+$/, "");
500
+ const c = this.component.lib;
501
+ const authorize = opts.authorize;
502
+ const guard = async (ctx, request) => {
503
+ if (authorize)
504
+ return { ok: await authorize(ctx, request), token: "" };
505
+ const token = globalThis.process?.env?.AI_BUDGET_DASHBOARD_TOKEN;
506
+ if (!token)
507
+ return { ok: false, token: "" };
508
+ const url = new URL(request.url);
509
+ const bearer = (request.headers.get("authorization") ?? "").replace(/^Bearer\s+/i, "");
510
+ // `?token=` is accepted only for the initial page navigation (a browser
511
+ // GET can't set headers); the page strips it from the URL on load and the
512
+ // JSON API is called with the bearer header. Compared in constant time.
513
+ const provided = bearer || url.searchParams.get("token") || "";
514
+ return { ok: timingSafeEqual(provided, token), token };
515
+ };
516
+ const json = (data, status = 200) => new Response(JSON.stringify(data ?? null), {
517
+ status,
518
+ headers: { "content-type": "application/json" },
519
+ });
520
+ const handle = async (ctx, request) => {
521
+ const url = new URL(request.url);
522
+ const sub = url.pathname.slice(prefix.length) || "/";
523
+ const { ok, token } = await guard(ctx, request);
524
+ if (!ok) {
525
+ return new Response("Unauthorized. Pass `authorize` to registerRoutes or set AI_BUDGET_DASHBOARD_TOKEN.", { status: 401 });
526
+ }
527
+ if (sub.startsWith("/api/")) {
528
+ const route = request.method + " " + sub.slice(4); // strip "/api"
529
+ const p = {};
530
+ url.searchParams.forEach((v, k) => {
531
+ p[k] = v;
532
+ });
533
+ const body = request.method === "POST"
534
+ ? await request.json().catch(() => ({}))
535
+ : {};
536
+ switch (route) {
537
+ case "GET /buckets":
538
+ return json(await ctx.runQuery(c.listBuckets, { dimension: p.dimension || undefined }));
539
+ case "GET /requests":
540
+ return json(await ctx.runQuery(c.listRequests, {
541
+ userId: p.userId || undefined,
542
+ dimension: p.dimension || undefined,
543
+ value: p.value || undefined,
544
+ limit: 100,
545
+ }));
546
+ case "GET /usage":
547
+ return json(await ctx.runQuery(c.usageHistory, {
548
+ dimension: p.dimension,
549
+ value: p.value,
550
+ period: p.period === "month" ? "month" : "day",
551
+ }));
552
+ case "GET /global":
553
+ return json(await ctx.runQuery(c.getGlobalStatus, {}));
554
+ case "GET /prices":
555
+ return json(await ctx.runQuery(c.listPrices, {}));
556
+ case "POST /setLimits":
557
+ return json(await ctx.runMutation(c.setBucketLimits, body));
558
+ case "POST /bump":
559
+ return json(await ctx.runMutation(c.bumpBucket, body));
560
+ case "POST /adjust":
561
+ return json(await ctx.runMutation(c.adjustBucket, body));
562
+ case "POST /delete":
563
+ return json(await ctx.runMutation(c.deleteBucket, body));
564
+ case "POST /global/setLimits":
565
+ return json(await ctx.runMutation(c.setGlobalLimits, body));
566
+ case "POST /global/setAlertDefaults":
567
+ return json(await ctx.runMutation(c.setAlertDefaults, body));
568
+ case "POST /global/setRetention":
569
+ return json(await ctx.runMutation(c.setRetention, body));
570
+ case "POST /setPrice":
571
+ return json(await ctx.runMutation(c.setPrice, body));
572
+ default:
573
+ return json({ error: "not found" }, 404);
574
+ }
575
+ }
576
+ // Inject as JSON literals (function replacers so `$` in the value isn't
577
+ // treated as a replacement pattern). This keeps a token/prefix containing
578
+ // quotes, backslashes, or `</script>` from breaking out of the JS string.
579
+ const html = DASHBOARD_HTML.replace(/__API_BASE__/g, () => JSON.stringify(`${prefix}/api`)).replace(/__TOKEN__/g, () => JSON.stringify(token));
580
+ return new Response(html, {
581
+ headers: { "content-type": "text/html; charset=utf-8" },
582
+ });
583
+ };
584
+ const handler = httpActionGeneric(handle);
585
+ http.route({ path: prefix, method: "GET", handler });
586
+ http.route({ pathPrefix: `${prefix}/`, method: "GET", handler });
587
+ http.route({ pathPrefix: `${prefix}/`, method: "POST", handler });
588
+ }
346
589
  }
347
590
  /** @deprecated Renamed to `AIBudget`. */
348
591
  export const WorryFreeAI = AIBudget;
@@ -20,22 +20,28 @@ import type { FunctionReference } from "convex/server";
20
20
  */
21
21
  export type ComponentApi<Name extends string | undefined = string | undefined> = {
22
22
  lib: {
23
- bumpAction: FunctionReference<"mutation", "internal", {
23
+ adjustBucket: FunctionReference<"mutation", "internal", {
24
+ deltaNanos: number;
25
+ dimension: string;
26
+ reason?: string;
27
+ tokens?: number;
28
+ value: string;
29
+ }, null, Name>;
30
+ bumpBucket: FunctionReference<"mutation", "internal", {
24
31
  dailyNanos?: number;
32
+ dimension: string;
25
33
  lifetimeNanos?: number;
26
- name: string;
34
+ monthlyNanos?: number;
35
+ value: string;
27
36
  }, null, Name>;
28
37
  bumpGlobal: FunctionReference<"mutation", "internal", {
29
38
  dailyNanos?: number;
30
39
  lifetimeNanos?: number;
40
+ monthlyNanos?: number;
31
41
  }, null, Name>;
32
- bumpUser: FunctionReference<"mutation", "internal", {
33
- dailyNanos?: number;
34
- lifetimeNanos?: number;
35
- userId: string;
36
- }, null, Name>;
37
- deleteUser: FunctionReference<"mutation", "internal", {
38
- userId: string;
42
+ deleteBucket: FunctionReference<"mutation", "internal", {
43
+ dimension: string;
44
+ value: string;
39
45
  }, {
40
46
  deletedThisBatch: number;
41
47
  done: boolean;
@@ -43,6 +49,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
43
49
  finishRequest: FunctionReference<"mutation", "internal", {
44
50
  cachedTokens?: number;
45
51
  completionTokens?: number;
52
+ costNanos?: number;
46
53
  error?: string;
47
54
  latencyMs?: number;
48
55
  promptTokens?: number;
@@ -51,10 +58,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
51
58
  }, {
52
59
  costNanos: number;
53
60
  }, Name>;
61
+ getBucket: FunctionReference<"query", "internal", {
62
+ dimension: string;
63
+ value: string;
64
+ }, any, Name>;
54
65
  getGlobalStatus: FunctionReference<"query", "internal", {}, {
55
66
  dailySpendLimitNanos: number | null;
67
+ defaultWarnAtPct: number | null;
56
68
  enforcement: "hard" | "soft";
57
69
  lifetimeSpendLimitNanos: number | null;
70
+ retentionMs: number | null;
58
71
  spentTodayNanos: number;
59
72
  spentTotalNanos: number;
60
73
  }, Name>;
@@ -68,42 +81,50 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
68
81
  lineage: FunctionReference<"query", "internal", {
69
82
  requestId: string;
70
83
  }, any, Name>;
71
- listActions: FunctionReference<"query", "internal", {}, any, Name>;
84
+ listAdjustments: FunctionReference<"query", "internal", {
85
+ dimension: string;
86
+ limit?: number;
87
+ value: string;
88
+ }, any, Name>;
89
+ listBuckets: FunctionReference<"query", "internal", {
90
+ dimension?: string;
91
+ }, any, Name>;
72
92
  listPrices: FunctionReference<"query", "internal", {}, any, Name>;
73
93
  listRequests: FunctionReference<"query", "internal", {
94
+ dimension?: string;
74
95
  limit?: number;
75
96
  userId?: string;
97
+ value?: string;
76
98
  }, any, Name>;
77
- listUsers: FunctionReference<"query", "internal", {}, any, Name>;
78
- setActionLimits: FunctionReference<"mutation", "internal", {
99
+ setAlertDefaults: FunctionReference<"mutation", "internal", {
100
+ warnAtPct?: number;
101
+ }, null, Name>;
102
+ setBucketLimits: FunctionReference<"mutation", "internal", {
103
+ blocked?: boolean;
79
104
  dailySpendLimitNanos?: number;
80
105
  dailyTokenLimit?: number;
81
- disabled?: boolean;
106
+ dimension: string;
82
107
  enforcement?: "hard" | "soft";
83
108
  lifetimeSpendLimitNanos?: number;
84
109
  lifetimeTokenLimit?: number;
85
- name: string;
110
+ maxConcurrent?: number;
111
+ monthlySpendLimitNanos?: number;
112
+ monthlyTokenLimit?: number;
113
+ requestsPerMinute?: number;
114
+ value: string;
115
+ warnAtPct?: number;
86
116
  }, null, Name>;
87
117
  setGlobalLimits: FunctionReference<"mutation", "internal", {
88
118
  dailySpendLimitNanos?: number;
89
119
  enforcement?: "hard" | "soft";
90
120
  lifetimeSpendLimitNanos?: number;
91
121
  }, null, Name>;
92
- setLimits: FunctionReference<"mutation", "internal", {
93
- blocked?: boolean;
94
- dailySpendLimitNanos?: number;
95
- dailyTokenLimit?: number;
96
- enforcement?: "hard" | "soft";
97
- lifetimeSpendLimitNanos?: number;
98
- lifetimeTokenLimit?: number;
99
- requestsPerMinute?: number;
100
- userId: string;
101
- }, null, Name>;
102
122
  setModelPolicy: FunctionReference<"mutation", "internal", {
103
123
  mode: "open" | "allowlist" | "denylist";
104
124
  models: Array<string>;
105
125
  }, null, Name>;
106
126
  setPrice: FunctionReference<"mutation", "internal", {
127
+ cachedNanosPerMTok?: number;
107
128
  inputNanosPerMTok: number;
108
129
  model: string;
109
130
  outputNanosPerMTok: number;
@@ -119,9 +140,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
119
140
  }>;
120
141
  model: string;
121
142
  rerunOf?: string;
143
+ tags?: Array<{
144
+ dimension: string;
145
+ value: string;
146
+ }>;
122
147
  userId: string;
123
148
  }, {
124
149
  allowed: true;
150
+ notices: Array<string>;
125
151
  requestId: string;
126
152
  warnings: Array<string>;
127
153
  } | {
@@ -129,5 +155,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
129
155
  code: string;
130
156
  reason: string;
131
157
  }, Name>;
158
+ usageHistory: FunctionReference<"query", "internal", {
159
+ dimension: string;
160
+ limit?: number;
161
+ period: "day" | "month";
162
+ value: string;
163
+ }, any, Name>;
132
164
  };
133
165
  };