@convex-dev/ai-budget 0.0.2-alpha.15 → 0.0.2-alpha.16

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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ full audit log you can replay later.
23
23
  - [Setup](#setup)
24
24
  - [Quickstart](#quickstart)
25
25
  - [Concepts](#concepts) — dimensions, nanodollars, reserve→settle
26
- - [Generating text](#generating-text) — `chat`, `languageModel`, replay
26
+ - [Generating text](#generating-text) — `chat`, `languageModel`, `meter`, `decisions`, replay
27
27
  - [Budgets & limits](#budgets--limits) — set caps, bumps, credits, alerts
28
28
  - [Monitoring](#monitoring) — totals, spend history, the request log
29
29
  - [Deployment-wide controls](#deployment-wide-controls) — global cap, model policy, pricing, retention
@@ -251,6 +251,35 @@ await ai.meter(ctx,
251
251
  });
252
252
  ```
253
253
 
254
+ ### `ai.decisions` — structured decisions (Jev)
255
+
256
+ Budget the gateway's Decisions endpoint ([Jev](https://docs.typesafe.ai)) — typed
257
+ `choice` / `score` / `boolean` questions evaluated against a `state` — with the
258
+ same limits, audit log, cost tracking, and tags as `ai.chat`:
259
+
260
+ ```ts
261
+ const { answers, costNanos } = await ai.decisions(ctx, {
262
+ state: { ticket: "Customer cannot sign in" },
263
+ questions: {
264
+ priority: {
265
+ type: "choice",
266
+ instructions: "Choose the response priority",
267
+ criteria: { urgent: "Respond now", normal: "Respond today" },
268
+ },
269
+ needsReview: { type: "boolean", instructions: "Does a human need to review this?" },
270
+ },
271
+ tags: [{ dimension: "team", value: "support" }],
272
+ });
273
+ answers.priority.choice; // "urgent" | "normal"
274
+ answers.needsReview.probability;
275
+ ```
276
+
277
+ Sugar over `ai.meter`, so cost is the gateway's authoritative amount. Model
278
+ defaults to `defaultEvalModel` (`"typesafe/jev-1.13"`). Requires
279
+ `@convex-dev/ai-sdk-provider` ≥ 0.2.1 and an `ai` version with
280
+ `experimental_evaluate` — both imported lazily, so callers who don't use
281
+ `decisions` are unaffected.
282
+
254
283
  ### `ai.begin` / `ai.settle` — long async jobs (video)
255
284
 
256
285
  A video job is submit → wait minutes → poll/webhook → done, spanning multiple
@@ -39,6 +39,8 @@ export type SoftLimitInfo = BudgetEventInfo & {
39
39
  };
40
40
  export type AIBudgetOptions = {
41
41
  defaultModel?: string;
42
+ /** Default model for `decisions()` (the Decisions/"Jev" endpoint). */
43
+ defaultEvalModel?: string;
42
44
  /**
43
45
  * A *soft* limit was exceeded (request still allowed). Lets you surface budget
44
46
  * warnings even on the languageModel/Agent path where they can't be returned.
@@ -82,6 +84,16 @@ export type ChatResult = {
82
84
  /** Approaching-cap notices (empty unless a warnAtPct threshold was crossed). */
83
85
  notices: string[];
84
86
  };
87
+ /** The tracked result of a `decisions()` call: budgeting metadata plus the
88
+ * structured answers from the Decisions ("Jev") endpoint. */
89
+ export type DecisionResult = Omit<ChatResult, "text"> & {
90
+ /** Structured answers keyed by your question names (shape depends on each
91
+ * question type: `choice`, `score`, or `boolean`). */
92
+ answers: Record<string, any>;
93
+ /** The raw gateway response, including provider-specific fields (e.g.
94
+ * `confidence`) under `response.body`. */
95
+ response?: any;
96
+ };
85
97
  /** Limits/controls settable on any budget bucket (user, action, or tag). */
86
98
  export type BucketLimits = {
87
99
  /** Token-bucket refill per minute and burst capacity; 0 blocks all requests. */
@@ -107,6 +119,7 @@ export type BumpArgs = {
107
119
  export declare class AIBudget {
108
120
  component: AIBudgetApi;
109
121
  defaultModel: string;
122
+ defaultEvalModel: string;
110
123
  private onSoftLimit?;
111
124
  private onThreshold?;
112
125
  private onLimitReached?;
@@ -208,6 +221,46 @@ export declare class AIBudget {
208
221
  /** Extra attribution dimensions to bill/limit (team, customer, env, …). */
209
222
  tags?: Tag[];
210
223
  }): Promise<ChatResult>;
224
+ /**
225
+ * Budget a structured decision through the AI Gateway's Decisions ("Jev")
226
+ * endpoint — sugar over `meter`. Evaluates typed `questions` (choice / score /
227
+ * boolean) about the `state` you provide, with the same reserve→settle
228
+ * limits, audit log, cost tracking, and per-tag attribution as `chat`. Call
229
+ * from an action. `userId` defaults to the authenticated caller.
230
+ *
231
+ * Requires `@convex-dev/ai-sdk-provider` >= 0.2.1 and an `ai` version that
232
+ * exposes `experimental_evaluate` (AI SDK 7's evaluation interface); both are
233
+ * imported lazily, so consumers who never call `decisions()` are unaffected.
234
+ *
235
+ * const { answers } = await ai.decisions(ctx, {
236
+ * state: { ticket: "Customer cannot sign in" },
237
+ * questions: {
238
+ * priority: { type: "choice", instructions: "...", criteria: { urgent: "...", normal: "..." } },
239
+ * needsReview: { type: "boolean", instructions: "..." },
240
+ * },
241
+ * });
242
+ * answers.priority.choice; // "urgent" | "normal"
243
+ */
244
+ decisions(ctx: RunMutationCtx, args: {
245
+ /** The evaluation model. Defaults to `defaultEvalModel` ("typesafe/jev-1.13"). */
246
+ model?: string;
247
+ /** Context the questions are evaluated against (a string or an object). */
248
+ state: unknown;
249
+ /** Typed questions (choice / score / boolean) keyed by name. */
250
+ questions: Record<string, unknown>;
251
+ /** Whom to bill. Defaults to the authenticated user (ctx.auth). */
252
+ userId?: string;
253
+ /** Attribute spend to this action name. Defaults to the calling action. */
254
+ action?: string;
255
+ /** Extra attribution dimensions to bill/limit (team, customer, env, …). */
256
+ tags?: Tag[];
257
+ /** Reserve this exact amount (nanodollars) up front — the decision cost
258
+ * isn't known before the call, so a hard cap is only exact with this. */
259
+ estimatedCostNanos?: number;
260
+ rerunOf?: string;
261
+ /** Cancel the underlying request. */
262
+ abortSignal?: AbortSignal;
263
+ }): Promise<DecisionResult>;
211
264
  /**
212
265
  * An AI SDK LanguageModel that enforces limits and records usage/cost for
213
266
  * `userId` on every call. Drop it into `generateText`, `streamText`, or the
@@ -115,12 +115,14 @@ function timingSafeEqual(a, b) {
115
115
  export class AIBudget {
116
116
  component;
117
117
  defaultModel;
118
+ defaultEvalModel;
118
119
  onSoftLimit;
119
120
  onThreshold;
120
121
  onLimitReached;
121
122
  constructor(component, options) {
122
123
  this.component = component;
123
124
  this.defaultModel = options?.defaultModel ?? "openai/gpt-4o-mini";
125
+ this.defaultEvalModel = options?.defaultEvalModel ?? "typesafe/jev-1.13";
124
126
  this.onSoftLimit = options?.onSoftLimit;
125
127
  this.onThreshold = options?.onThreshold;
126
128
  this.onLimitReached = options?.onLimitReached;
@@ -302,6 +304,72 @@ export class AIBudget {
302
304
  };
303
305
  });
304
306
  }
307
+ /**
308
+ * Budget a structured decision through the AI Gateway's Decisions ("Jev")
309
+ * endpoint — sugar over `meter`. Evaluates typed `questions` (choice / score /
310
+ * boolean) about the `state` you provide, with the same reserve→settle
311
+ * limits, audit log, cost tracking, and per-tag attribution as `chat`. Call
312
+ * from an action. `userId` defaults to the authenticated caller.
313
+ *
314
+ * Requires `@convex-dev/ai-sdk-provider` >= 0.2.1 and an `ai` version that
315
+ * exposes `experimental_evaluate` (AI SDK 7's evaluation interface); both are
316
+ * imported lazily, so consumers who never call `decisions()` are unaffected.
317
+ *
318
+ * const { answers } = await ai.decisions(ctx, {
319
+ * state: { ticket: "Customer cannot sign in" },
320
+ * questions: {
321
+ * priority: { type: "choice", instructions: "...", criteria: { urgent: "...", normal: "..." } },
322
+ * needsReview: { type: "boolean", instructions: "..." },
323
+ * },
324
+ * });
325
+ * answers.priority.choice; // "urgent" | "normal"
326
+ */
327
+ async decisions(ctx, args) {
328
+ const model = args.model ?? this.defaultEvalModel;
329
+ // `evaluate` is an experimental, version-gated export; import it lazily and
330
+ // untyped so consumers on an older `ai` (who never call this) aren't broken.
331
+ const evaluate = (await import("ai")).experimental_evaluate;
332
+ if (typeof evaluate !== "function") {
333
+ throw new Error("ai-budget: decisions() needs `experimental_evaluate` from the `ai` " +
334
+ "package (AI SDK 7's evaluation interface). Upgrade `ai` to a " +
335
+ "version that exports it.");
336
+ }
337
+ // Likewise, `evaluationModel` exists on @convex-dev/ai-sdk-provider >= 0.2.1.
338
+ const evaluationModel = convexGateway.evaluationModel;
339
+ if (typeof evaluationModel !== "function") {
340
+ throw new Error("ai-budget: decisions() needs `convexGateway.evaluationModel` from " +
341
+ "@convex-dev/ai-sdk-provider >= 0.2.1. Upgrade the provider.");
342
+ }
343
+ let decision;
344
+ const result = await this.meter(ctx, {
345
+ model,
346
+ // Store the structured request for audit/replay.
347
+ messages: [
348
+ {
349
+ role: "user",
350
+ content: JSON.stringify({ state: args.state, questions: args.questions }),
351
+ },
352
+ ],
353
+ userId: args.userId,
354
+ action: args.action,
355
+ tags: args.tags,
356
+ estimatedCostNanos: args.estimatedCostNanos,
357
+ rerunOf: args.rerunOf,
358
+ }, async () => {
359
+ decision = await evaluate({
360
+ model: evaluationModel(model),
361
+ state: args.state,
362
+ questions: args.questions,
363
+ ...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
364
+ });
365
+ return {
366
+ usage: decision?.usage,
367
+ costNanos: extractGatewayCostNanos(decision),
368
+ };
369
+ });
370
+ const { text: _text, ...tracking } = result;
371
+ return { ...tracking, answers: decision?.answers ?? {}, response: decision?.response };
372
+ }
305
373
  /**
306
374
  * An AI SDK LanguageModel that enforces limits and records usage/cost for
307
375
  * `userId` on every call. Drop it into `generateText`, `streamText`, or the
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.15",
10
+ "version": "0.0.2-alpha.16",
11
11
  "license": "Apache-2.0",
12
12
  "type": "module",
13
13
  "keywords": [
@@ -71,6 +71,8 @@ export type BudgetEventInfo = {
71
71
  export type SoftLimitInfo = BudgetEventInfo & { warnings: string[] };
72
72
  export type AIBudgetOptions = {
73
73
  defaultModel?: string;
74
+ /** Default model for `decisions()` (the Decisions/"Jev" endpoint). */
75
+ defaultEvalModel?: string;
74
76
  /**
75
77
  * A *soft* limit was exceeded (request still allowed). Lets you surface budget
76
78
  * warnings even on the languageModel/Agent path where they can't be returned.
@@ -143,6 +145,17 @@ export type ChatResult = {
143
145
  notices: string[];
144
146
  };
145
147
 
148
+ /** The tracked result of a `decisions()` call: budgeting metadata plus the
149
+ * structured answers from the Decisions ("Jev") endpoint. */
150
+ export type DecisionResult = Omit<ChatResult, "text"> & {
151
+ /** Structured answers keyed by your question names (shape depends on each
152
+ * question type: `choice`, `score`, or `boolean`). */
153
+ answers: Record<string, any>;
154
+ /** The raw gateway response, including provider-specific fields (e.g.
155
+ * `confidence`) under `response.body`. */
156
+ response?: any;
157
+ };
158
+
146
159
  // ---------- helpers ----------
147
160
 
148
161
  // Token counts across AI SDK versions come as plain numbers or, in v7, as a
@@ -262,6 +275,7 @@ export type BumpArgs = {
262
275
 
263
276
  export class AIBudget {
264
277
  public defaultModel: string;
278
+ public defaultEvalModel: string;
265
279
  private onSoftLimit?: AIBudgetOptions["onSoftLimit"];
266
280
  private onThreshold?: AIBudgetOptions["onThreshold"];
267
281
  private onLimitReached?: AIBudgetOptions["onLimitReached"];
@@ -270,6 +284,7 @@ export class AIBudget {
270
284
  options?: AIBudgetOptions
271
285
  ) {
272
286
  this.defaultModel = options?.defaultModel ?? "openai/gpt-4o-mini";
287
+ this.defaultEvalModel = options?.defaultEvalModel ?? "typesafe/jev-1.13";
273
288
  this.onSoftLimit = options?.onSoftLimit;
274
289
  this.onThreshold = options?.onThreshold;
275
290
  this.onLimitReached = options?.onLimitReached;
@@ -534,6 +549,103 @@ export class AIBudget {
534
549
  );
535
550
  }
536
551
 
552
+ /**
553
+ * Budget a structured decision through the AI Gateway's Decisions ("Jev")
554
+ * endpoint — sugar over `meter`. Evaluates typed `questions` (choice / score /
555
+ * boolean) about the `state` you provide, with the same reserve→settle
556
+ * limits, audit log, cost tracking, and per-tag attribution as `chat`. Call
557
+ * from an action. `userId` defaults to the authenticated caller.
558
+ *
559
+ * Requires `@convex-dev/ai-sdk-provider` >= 0.2.1 and an `ai` version that
560
+ * exposes `experimental_evaluate` (AI SDK 7's evaluation interface); both are
561
+ * imported lazily, so consumers who never call `decisions()` are unaffected.
562
+ *
563
+ * const { answers } = await ai.decisions(ctx, {
564
+ * state: { ticket: "Customer cannot sign in" },
565
+ * questions: {
566
+ * priority: { type: "choice", instructions: "...", criteria: { urgent: "...", normal: "..." } },
567
+ * needsReview: { type: "boolean", instructions: "..." },
568
+ * },
569
+ * });
570
+ * answers.priority.choice; // "urgent" | "normal"
571
+ */
572
+ async decisions(
573
+ ctx: RunMutationCtx,
574
+ args: {
575
+ /** The evaluation model. Defaults to `defaultEvalModel` ("typesafe/jev-1.13"). */
576
+ model?: string;
577
+ /** Context the questions are evaluated against (a string or an object). */
578
+ state: unknown;
579
+ /** Typed questions (choice / score / boolean) keyed by name. */
580
+ questions: Record<string, unknown>;
581
+ /** Whom to bill. Defaults to the authenticated user (ctx.auth). */
582
+ userId?: string;
583
+ /** Attribute spend to this action name. Defaults to the calling action. */
584
+ action?: string;
585
+ /** Extra attribution dimensions to bill/limit (team, customer, env, …). */
586
+ tags?: Tag[];
587
+ /** Reserve this exact amount (nanodollars) up front — the decision cost
588
+ * isn't known before the call, so a hard cap is only exact with this. */
589
+ estimatedCostNanos?: number;
590
+ rerunOf?: string;
591
+ /** Cancel the underlying request. */
592
+ abortSignal?: AbortSignal;
593
+ }
594
+ ): Promise<DecisionResult> {
595
+ const model = args.model ?? this.defaultEvalModel;
596
+ // `evaluate` is an experimental, version-gated export; import it lazily and
597
+ // untyped so consumers on an older `ai` (who never call this) aren't broken.
598
+ const evaluate = ((await import("ai")) as any).experimental_evaluate;
599
+ if (typeof evaluate !== "function") {
600
+ throw new Error(
601
+ "ai-budget: decisions() needs `experimental_evaluate` from the `ai` " +
602
+ "package (AI SDK 7's evaluation interface). Upgrade `ai` to a " +
603
+ "version that exports it."
604
+ );
605
+ }
606
+ // Likewise, `evaluationModel` exists on @convex-dev/ai-sdk-provider >= 0.2.1.
607
+ const evaluationModel = (convexGateway as any).evaluationModel;
608
+ if (typeof evaluationModel !== "function") {
609
+ throw new Error(
610
+ "ai-budget: decisions() needs `convexGateway.evaluationModel` from " +
611
+ "@convex-dev/ai-sdk-provider >= 0.2.1. Upgrade the provider."
612
+ );
613
+ }
614
+ let decision: any;
615
+ const result = await this.meter(
616
+ ctx,
617
+ {
618
+ model,
619
+ // Store the structured request for audit/replay.
620
+ messages: [
621
+ {
622
+ role: "user",
623
+ content: JSON.stringify({ state: args.state, questions: args.questions }),
624
+ },
625
+ ],
626
+ userId: args.userId,
627
+ action: args.action,
628
+ tags: args.tags,
629
+ estimatedCostNanos: args.estimatedCostNanos,
630
+ rerunOf: args.rerunOf,
631
+ },
632
+ async () => {
633
+ decision = await evaluate({
634
+ model: evaluationModel(model),
635
+ state: args.state,
636
+ questions: args.questions,
637
+ ...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
638
+ });
639
+ return {
640
+ usage: decision?.usage,
641
+ costNanos: extractGatewayCostNanos(decision),
642
+ };
643
+ }
644
+ );
645
+ const { text: _text, ...tracking } = result;
646
+ return { ...tracking, answers: decision?.answers ?? {}, response: decision?.response };
647
+ }
648
+
537
649
  /**
538
650
  * An AI SDK LanguageModel that enforces limits and records usage/cost for
539
651
  * `userId` on every call. Drop it into `generateText`, `streamText`, or the