@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 +30 -1
- package/dist/client/index.d.ts +53 -0
- package/dist/client/index.js +68 -0
- package/package.json +1 -1
- package/src/client/index.ts +112 -0
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
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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
|
package/dist/client/index.js
CHANGED
|
@@ -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
package/src/client/index.ts
CHANGED
|
@@ -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
|