@convex-dev/ai-budget 0.0.2-alpha.4 → 0.0.2-alpha.6

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,30 @@ 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 gateway doesn't report a dollar cost today, but if a future response ever
55
+ // carries an unambiguous nanodollar cost we pass it straight through as
56
+ // authoritative (finishRequest prefers it over the token-based estimate). Only
57
+ // an explicitly nano-denominated field is trusted — a bare `cost` could be in
58
+ // dollars and silently mis-bill by 1e9×.
59
+ function extractGatewayCostNanos(result) {
60
+ const meta = result?.providerMetadata?.convexGateway;
61
+ const candidates = [meta?.costNanos, result?.usage?.costNanos];
62
+ for (const c of candidates) {
63
+ if (typeof c === "number" && Number.isFinite(c) && c >= 0)
64
+ return c;
65
+ }
66
+ return undefined;
67
+ }
50
68
  // Flatten an AI SDK prompt (roles + content parts) into simple storable messages.
51
69
  function simplifyPrompt(prompt) {
52
70
  if (!Array.isArray(prompt))
@@ -78,24 +96,46 @@ function extractText(result) {
78
96
  }
79
97
  return "";
80
98
  }
81
- // ---------- client ----------
82
99
  export class AIBudget {
83
100
  component;
84
101
  defaultModel;
85
102
  onSoftLimit;
103
+ onThreshold;
104
+ onLimitReached;
86
105
  constructor(component, options) {
87
106
  this.component = component;
88
107
  this.defaultModel = options?.defaultModel ?? "openai/gpt-4o-mini";
89
108
  this.onSoftLimit = options?.onSoftLimit;
109
+ this.onThreshold = options?.onThreshold;
110
+ this.onLimitReached = options?.onLimitReached;
111
+ }
112
+ // Fire the soft-limit + threshold callbacks from a startRequest result.
113
+ async fireBudgetEvents(base, warnings, notices) {
114
+ if (warnings.length > 0 && this.onSoftLimit) {
115
+ try {
116
+ await this.onSoftLimit({ ...base, messages: warnings, warnings });
117
+ }
118
+ catch {
119
+ /* never let a callback error break a request */
120
+ }
121
+ }
122
+ if (notices.length > 0 && this.onThreshold) {
123
+ try {
124
+ await this.onThreshold({ ...base, messages: notices });
125
+ }
126
+ catch {
127
+ /* swallow */
128
+ }
129
+ }
90
130
  }
91
- async fireSoftLimit(info) {
92
- if (info.warnings.length === 0 || !this.onSoftLimit)
131
+ async fireLimitReached(info) {
132
+ if (!this.onLimitReached)
93
133
  return;
94
134
  try {
95
- await this.onSoftLimit(info);
135
+ await this.onLimitReached(info);
96
136
  }
97
137
  catch {
98
- // never let a callback error break a request
138
+ /* swallow */
99
139
  }
100
140
  }
101
141
  /**
@@ -116,6 +156,14 @@ export class AIBudget {
116
156
  rerunOf: args.rerunOf,
117
157
  });
118
158
  if (!started.allowed) {
159
+ await this.fireLimitReached({
160
+ userId,
161
+ action: actionName,
162
+ tags: args.tags,
163
+ messages: [started.reason],
164
+ code: started.code,
165
+ reason: started.reason,
166
+ });
119
167
  throw new ConvexError({
120
168
  kind: "AIBudgetLimit",
121
169
  code: started.code,
@@ -124,13 +172,8 @@ export class AIBudget {
124
172
  }
125
173
  const requestId = started.requestId;
126
174
  const warnings = started.warnings;
127
- await this.fireSoftLimit({
128
- userId,
129
- action: actionName,
130
- tags: args.tags,
131
- requestId,
132
- warnings,
133
- });
175
+ const notices = started.notices;
176
+ await this.fireBudgetEvents({ userId, action: actionName, tags: args.tags, requestId }, warnings, notices);
134
177
  const start = Date.now();
135
178
  try {
136
179
  // The full chain (incl. system) is stored on the request for audit/replay,
@@ -150,9 +193,10 @@ export class AIBudget {
150
193
  requestId,
151
194
  responseText: result.text,
152
195
  ...usage,
196
+ costNanos: extractGatewayCostNanos(result),
153
197
  latencyMs: Date.now() - start,
154
198
  });
155
- return { text: result.text, requestId, costNanos, warnings, ...usage };
199
+ return { text: result.text, requestId, costNanos, warnings, notices, ...usage };
156
200
  }
157
201
  catch (e) {
158
202
  await ctx.runMutation(this.component.lib.finishRequest, {
@@ -172,10 +216,12 @@ export class AIBudget {
172
216
  languageModel(ctx, opts = {}) {
173
217
  const modelId = opts.model ?? this.defaultModel;
174
218
  const component = this.component;
175
- const fireSoftLimit = this.fireSoftLimit.bind(this);
219
+ const fireBudgetEvents = this.fireBudgetEvents.bind(this);
220
+ const fireLimitReached = this.fireLimitReached.bind(this);
176
221
  const begin = async (params) => {
177
222
  const userId = await resolveUserId(ctx, opts.userId);
178
223
  const actionName = await resolveActionName(ctx, opts.action);
224
+ const base = { userId, action: actionName, tags: opts.tags };
179
225
  const started = await ctx.runMutation(component.lib.startRequest, {
180
226
  userId,
181
227
  actionName,
@@ -184,19 +230,19 @@ export class AIBudget {
184
230
  messages: simplifyPrompt(params.prompt),
185
231
  });
186
232
  if (!started.allowed) {
233
+ await fireLimitReached({
234
+ ...base,
235
+ messages: [started.reason],
236
+ code: started.code,
237
+ reason: started.reason,
238
+ });
187
239
  throw new ConvexError({
188
240
  kind: "AIBudgetLimit",
189
241
  code: started.code,
190
242
  reason: started.reason,
191
243
  });
192
244
  }
193
- await fireSoftLimit({
194
- userId,
195
- action: actionName,
196
- tags: opts.tags,
197
- requestId: started.requestId,
198
- warnings: started.warnings,
199
- });
245
+ await fireBudgetEvents({ ...base, requestId: started.requestId }, started.warnings, started.notices);
200
246
  return started.requestId;
201
247
  };
202
248
  const finish = async (requestId, fields) => ctx.runMutation(component.lib.finishRequest, { requestId, ...fields });
@@ -211,6 +257,7 @@ export class AIBudget {
211
257
  await finish(requestId, {
212
258
  responseText: extractText(result),
213
259
  ...extractUsage(result.usage),
260
+ costNanos: extractGatewayCostNanos(result),
214
261
  latencyMs: Date.now() - start,
215
262
  });
216
263
  return result;
@@ -295,6 +342,7 @@ export class AIBudget {
295
342
  get requests() {
296
343
  const c = this.component;
297
344
  return {
345
+ /** Filter by userId, or by any {dimension, value} (incl. custom tags). */
298
346
  list: (ctx, args = {}) => ctx.runQuery(c.lib.listRequests, args),
299
347
  /** Ancestors up to the original, plus direct re-runs. */
300
348
  lineage: (ctx, args) => ctx.runQuery(c.lib.lineage, { requestId: args.requestId }),
@@ -307,74 +355,73 @@ export class AIBudget {
307
355
  * generalization of `users`/`actions`. Give it any dimension name (team,
308
356
  * project, tenant, customer, env, feature, …) and set caps per value:
309
357
  *
310
- * ai.tag("customer").setLimits(ctx, { value: "acme", dailySpendLimitNanos });
311
- * ai.tag("customer").list(ctx);
358
+ * ai.tag("customer").setLimits(ctx, { value: "acme", monthlySpendLimitNanos });
359
+ * ai.tag("customer").history(ctx, { value: "acme", period: "day" });
312
360
  *
313
361
  * Attribute a call to it by passing `tags` to `chat`/`languageModel`.
314
362
  */
315
363
  tag(dimension) {
364
+ return this.dimensionApi(dimension, (a) => a.value);
365
+ }
366
+ // Shared implementation behind tag()/users/actions. `key` maps the namespace's
367
+ // id field (value/userId/name) to the bucket value.
368
+ dimensionApi(dimension, key) {
316
369
  const c = this.component;
317
370
  return {
318
371
  /** All buckets in this dimension. */
319
372
  list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension }),
320
373
  /** One bucket's limits + spend (null if it has none yet). */
321
- get: (ctx, args) => ctx.runQuery(c.lib.getBucket, { dimension, value: args.value }),
322
- setLimits: (ctx, args) => ctx.runMutation(c.lib.setBucketLimits, { dimension, ...args }),
323
- /** One-time "approve another $X" bump (daily is today-only). */
324
- bump: (ctx, args) => ctx.runMutation(c.lib.bumpBucket, { dimension, ...args }),
325
- /** Delete the bucket (for "user", also its request rows). */
326
- delete: (ctx, args) => ctx.runMutation(c.lib.deleteBucket, { dimension, value: args.value }),
327
- };
328
- }
329
- /** Per-user budgets and controls — sugar over the "user" dimension. */
330
- get users() {
331
- const c = this.component;
332
- return {
333
- list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension: "user" }),
374
+ get: (ctx, args) => ctx.runQuery(c.lib.getBucket, { dimension, value: key(args) }),
334
375
  setLimits: (ctx, args) => {
335
- const { userId, ...limits } = args;
376
+ const { value, userId, name, ...limits } = args;
336
377
  return ctx.runMutation(c.lib.setBucketLimits, {
337
- dimension: "user",
338
- value: userId,
378
+ dimension,
379
+ value: key(args),
339
380
  ...limits,
340
381
  });
341
382
  },
342
- /** One-time "approve another $X" bump (daily is today-only). */
383
+ /** One-time "approve another $X" bump (daily/monthly reset with the window). */
343
384
  bump: (ctx, args) => ctx.runMutation(c.lib.bumpBucket, {
344
- dimension: "user",
345
- value: args.userId,
385
+ dimension,
386
+ value: key(args),
346
387
  dailyNanos: args.dailyNanos,
388
+ monthlyNanos: args.monthlyNanos,
347
389
  lifetimeNanos: args.lifetimeNanos,
348
390
  }),
349
- /** Delete a user and all their request rows. */
350
- delete: (ctx, args) => ctx.runMutation(c.lib.deleteBucket, {
351
- dimension: "user",
352
- value: args.userId,
391
+ /** Manually credit (negative) or debit (positive) this bucket. */
392
+ adjust: (ctx, args) => ctx.runMutation(c.lib.adjustBucket, {
393
+ dimension,
394
+ value: key(args),
395
+ deltaNanos: args.deltaNanos,
396
+ tokens: args.tokens,
397
+ reason: args.reason,
398
+ }),
399
+ /** Durable spend history for this bucket (per day or per month). */
400
+ history: (ctx, args) => ctx.runQuery(c.lib.usageHistory, {
401
+ dimension,
402
+ value: key(args),
403
+ period: args.period ?? "day",
404
+ limit: args.limit,
353
405
  }),
406
+ /** Manual-adjustment audit log for this bucket. */
407
+ adjustments: (ctx, args) => ctx.runQuery(c.lib.listAdjustments, {
408
+ dimension,
409
+ value: key(args),
410
+ limit: args.limit,
411
+ }),
412
+ /** Delete the bucket (for "user", also its request rows). */
413
+ delete: (ctx, args) => ctx.runMutation(c.lib.deleteBucket, { dimension, value: key(args) }),
354
414
  };
355
415
  }
416
+ /** Per-user budgets and controls — sugar over the "user" dimension. */
417
+ get users() {
418
+ return this.dimensionApi("user", (a) => a.userId);
419
+ }
356
420
  /** Per-action (per-feature) budgets — sugar over the "action" dimension. */
357
421
  get actions() {
358
- const c = this.component;
359
- return {
360
- list: (ctx) => ctx.runQuery(c.lib.listBuckets, { dimension: "action" }),
361
- setLimits: (ctx, args) => {
362
- const { name, ...limits } = args;
363
- return ctx.runMutation(c.lib.setBucketLimits, {
364
- dimension: "action",
365
- value: name,
366
- ...limits,
367
- });
368
- },
369
- bump: (ctx, args) => ctx.runMutation(c.lib.bumpBucket, {
370
- dimension: "action",
371
- value: args.name,
372
- dailyNanos: args.dailyNanos,
373
- lifetimeNanos: args.lifetimeNanos,
374
- }),
375
- };
422
+ return this.dimensionApi("action", (a) => a.name);
376
423
  }
377
- /** The deployment-wide budget and retention config. */
424
+ /** The deployment-wide budget, alerts, and retention config. */
378
425
  get global() {
379
426
  const c = this.component;
380
427
  return {
@@ -383,6 +430,8 @@ export class AIBudget {
383
430
  /** A killswitch spend cap across all users/actions (enforced approximately). */
384
431
  setLimits: (ctx, args) => ctx.runMutation(c.lib.setGlobalLimits, args),
385
432
  bump: (ctx, args) => ctx.runMutation(c.lib.bumpGlobal, args),
433
+ /** Default approaching-limit alert threshold (fraction of a cap, e.g. 0.8). */
434
+ setAlertDefaults: (ctx, args) => ctx.runMutation(c.lib.setAlertDefaults, args),
386
435
  /** Request-row retention window in ms (default 1h; 0 disables). */
387
436
  setRetention: (ctx, args) => ctx.runMutation(c.lib.setRetention, args),
388
437
  };
@@ -404,6 +453,111 @@ export class AIBudget {
404
453
  set: (ctx, args) => ctx.runMutation(c.lib.setPrice, args),
405
454
  };
406
455
  }
456
+ /**
457
+ * Mount the built-in admin dashboard on your app's HTTP router with one call.
458
+ * Serves a self-contained HTML dashboard (buckets, requests, usage history,
459
+ * settings) plus a small JSON API, all backed by the component — no extra
460
+ * queries to write.
461
+ *
462
+ * // convex/http.ts
463
+ * import { httpRouter } from "convex/server";
464
+ * const http = httpRouter();
465
+ * ai.registerRoutes(http, { authorize: async (ctx) =>
466
+ * (await ctx.auth.getUserIdentity())?.role === "admin" });
467
+ * export default http;
468
+ *
469
+ * It then lives at `https://<deployment>.convex.site/aibudget`.
470
+ *
471
+ * SECURITY: the endpoint is public on the internet. You MUST gate it — either
472
+ * pass `authorize` (recommended: check the caller is a deployment admin) or
473
+ * set the `AI_BUDGET_DASHBOARD_TOKEN` env var (a bearer token / `?token=`).
474
+ * With neither, every route returns 401.
475
+ */
476
+ registerRoutes(http, opts = {}) {
477
+ const prefix = (opts.path ?? "/aibudget").replace(/\/+$/, "");
478
+ const c = this.component.lib;
479
+ const authorize = opts.authorize;
480
+ const guard = async (ctx, request) => {
481
+ if (authorize)
482
+ return { ok: await authorize(ctx, request), token: "" };
483
+ const token = globalThis.process?.env?.AI_BUDGET_DASHBOARD_TOKEN;
484
+ if (!token)
485
+ return { ok: false, token: "" };
486
+ const url = new URL(request.url);
487
+ const bearer = (request.headers.get("authorization") ?? "").replace(/^Bearer\s+/i, "");
488
+ const provided = bearer || url.searchParams.get("token") || "";
489
+ return { ok: provided === token, token };
490
+ };
491
+ const json = (data, status = 200) => new Response(JSON.stringify(data ?? null), {
492
+ status,
493
+ headers: { "content-type": "application/json" },
494
+ });
495
+ const handle = async (ctx, request) => {
496
+ const url = new URL(request.url);
497
+ const sub = url.pathname.slice(prefix.length) || "/";
498
+ const { ok, token } = await guard(ctx, request);
499
+ if (!ok) {
500
+ return new Response("Unauthorized. Pass `authorize` to registerRoutes or set AI_BUDGET_DASHBOARD_TOKEN.", { status: 401 });
501
+ }
502
+ if (sub.startsWith("/api/")) {
503
+ const route = request.method + " " + sub.slice(4); // strip "/api"
504
+ const p = {};
505
+ url.searchParams.forEach((v, k) => {
506
+ p[k] = v;
507
+ });
508
+ const body = request.method === "POST"
509
+ ? await request.json().catch(() => ({}))
510
+ : {};
511
+ switch (route) {
512
+ case "GET /buckets":
513
+ return json(await ctx.runQuery(c.listBuckets, { dimension: p.dimension || undefined }));
514
+ case "GET /requests":
515
+ return json(await ctx.runQuery(c.listRequests, {
516
+ userId: p.userId || undefined,
517
+ dimension: p.dimension || undefined,
518
+ value: p.value || undefined,
519
+ limit: 100,
520
+ }));
521
+ case "GET /usage":
522
+ return json(await ctx.runQuery(c.usageHistory, {
523
+ dimension: p.dimension,
524
+ value: p.value,
525
+ period: p.period === "month" ? "month" : "day",
526
+ }));
527
+ case "GET /global":
528
+ return json(await ctx.runQuery(c.getGlobalStatus, {}));
529
+ case "GET /prices":
530
+ return json(await ctx.runQuery(c.listPrices, {}));
531
+ case "POST /setLimits":
532
+ return json(await ctx.runMutation(c.setBucketLimits, body));
533
+ case "POST /bump":
534
+ return json(await ctx.runMutation(c.bumpBucket, body));
535
+ case "POST /adjust":
536
+ return json(await ctx.runMutation(c.adjustBucket, body));
537
+ case "POST /delete":
538
+ return json(await ctx.runMutation(c.deleteBucket, body));
539
+ case "POST /global/setLimits":
540
+ return json(await ctx.runMutation(c.setGlobalLimits, body));
541
+ case "POST /global/setAlertDefaults":
542
+ return json(await ctx.runMutation(c.setAlertDefaults, body));
543
+ case "POST /global/setRetention":
544
+ return json(await ctx.runMutation(c.setRetention, body));
545
+ case "POST /setPrice":
546
+ return json(await ctx.runMutation(c.setPrice, body));
547
+ default:
548
+ return json({ error: "not found" }, 404);
549
+ }
550
+ }
551
+ const html = DASHBOARD_HTML.replace(/__API_BASE__/g, `${prefix}/api`).replace(/__TOKEN__/g, token);
552
+ return new Response(html, {
553
+ headers: { "content-type": "text/html; charset=utf-8" },
554
+ });
555
+ };
556
+ const handler = httpActionGeneric(handle);
557
+ http.route({ path: prefix, method: "GET", handler });
558
+ http.route({ pathPrefix: `${prefix}/`, method: "GET", handler });
559
+ http.route({ pathPrefix: `${prefix}/`, method: "POST", handler });
560
+ }
407
561
  }
408
562
  /** @deprecated Renamed to `AIBudget`. */
409
563
  export const WorryFreeAI = AIBudget;
@@ -20,15 +20,24 @@ import type { FunctionReference } from "convex/server";
20
20
  */
21
21
  export type ComponentApi<Name extends string | undefined = string | undefined> = {
22
22
  lib: {
23
+ adjustBucket: FunctionReference<"mutation", "internal", {
24
+ deltaNanos: number;
25
+ dimension: string;
26
+ reason?: string;
27
+ tokens?: number;
28
+ value: string;
29
+ }, null, Name>;
23
30
  bumpBucket: FunctionReference<"mutation", "internal", {
24
31
  dailyNanos?: number;
25
32
  dimension: string;
26
33
  lifetimeNanos?: number;
34
+ monthlyNanos?: number;
27
35
  value: string;
28
36
  }, null, Name>;
29
37
  bumpGlobal: FunctionReference<"mutation", "internal", {
30
38
  dailyNanos?: number;
31
39
  lifetimeNanos?: number;
40
+ monthlyNanos?: number;
32
41
  }, null, Name>;
33
42
  deleteBucket: FunctionReference<"mutation", "internal", {
34
43
  dimension: string;
@@ -40,6 +49,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
40
49
  finishRequest: FunctionReference<"mutation", "internal", {
41
50
  cachedTokens?: number;
42
51
  completionTokens?: number;
52
+ costNanos?: number;
43
53
  error?: string;
44
54
  latencyMs?: number;
45
55
  promptTokens?: number;
@@ -54,8 +64,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
54
64
  }, any, Name>;
55
65
  getGlobalStatus: FunctionReference<"query", "internal", {}, {
56
66
  dailySpendLimitNanos: number | null;
67
+ defaultWarnAtPct: number | null;
57
68
  enforcement: "hard" | "soft";
58
69
  lifetimeSpendLimitNanos: number | null;
70
+ retentionMs: number | null;
59
71
  spentTodayNanos: number;
60
72
  spentTotalNanos: number;
61
73
  }, Name>;
@@ -69,14 +81,24 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
69
81
  lineage: FunctionReference<"query", "internal", {
70
82
  requestId: string;
71
83
  }, any, Name>;
84
+ listAdjustments: FunctionReference<"query", "internal", {
85
+ dimension: string;
86
+ limit?: number;
87
+ value: string;
88
+ }, any, Name>;
72
89
  listBuckets: FunctionReference<"query", "internal", {
73
90
  dimension?: string;
74
91
  }, any, Name>;
75
92
  listPrices: FunctionReference<"query", "internal", {}, any, Name>;
76
93
  listRequests: FunctionReference<"query", "internal", {
94
+ dimension?: string;
77
95
  limit?: number;
78
96
  userId?: string;
97
+ value?: string;
79
98
  }, any, Name>;
99
+ setAlertDefaults: FunctionReference<"mutation", "internal", {
100
+ warnAtPct?: number;
101
+ }, null, Name>;
80
102
  setBucketLimits: FunctionReference<"mutation", "internal", {
81
103
  blocked?: boolean;
82
104
  dailySpendLimitNanos?: number;
@@ -85,8 +107,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
85
107
  enforcement?: "hard" | "soft";
86
108
  lifetimeSpendLimitNanos?: number;
87
109
  lifetimeTokenLimit?: number;
110
+ maxConcurrent?: number;
111
+ monthlySpendLimitNanos?: number;
112
+ monthlyTokenLimit?: number;
88
113
  requestsPerMinute?: number;
89
114
  value: string;
115
+ warnAtPct?: number;
90
116
  }, null, Name>;
91
117
  setGlobalLimits: FunctionReference<"mutation", "internal", {
92
118
  dailySpendLimitNanos?: number;
@@ -98,6 +124,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
98
124
  models: Array<string>;
99
125
  }, null, Name>;
100
126
  setPrice: FunctionReference<"mutation", "internal", {
127
+ cachedNanosPerMTok?: number;
101
128
  inputNanosPerMTok: number;
102
129
  model: string;
103
130
  outputNanosPerMTok: number;
@@ -120,6 +147,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
120
147
  userId: string;
121
148
  }, {
122
149
  allowed: true;
150
+ notices: Array<string>;
123
151
  requestId: string;
124
152
  warnings: Array<string>;
125
153
  } | {
@@ -127,5 +155,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
127
155
  code: string;
128
156
  reason: string;
129
157
  }, Name>;
158
+ usageHistory: FunctionReference<"query", "internal", {
159
+ dimension: string;
160
+ limit?: number;
161
+ period: "day" | "month";
162
+ value: string;
163
+ }, any, Name>;
130
164
  };
131
165
  };