@convex-dev/ai-budget 0.0.2-alpha.8 → 0.0.2-alpha.9

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.
@@ -51,20 +51,25 @@ function extractUsage(usage) {
51
51
  usage?.cached_tokens),
52
52
  };
53
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×.
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
59
  function extractGatewayCostNanos(result) {
60
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;
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);
65
69
  }
66
70
  return undefined;
67
71
  }
72
+ const NANOS_PER_DOLLAR = 1e9;
68
73
  // Flatten an AI SDK prompt (roles + content parts) into simple storable messages.
69
74
  function simplifyPrompt(prompt) {
70
75
  if (!Array.isArray(prompt))
@@ -286,6 +291,7 @@ export class AIBudget {
286
291
  const start = Date.now();
287
292
  let text = "";
288
293
  let usage = undefined;
294
+ let providerMetadata = undefined;
289
295
  try {
290
296
  const result = await doStream();
291
297
  // finishRequest is idempotent (terminal-guarded server-side), so
@@ -302,6 +308,7 @@ export class AIBudget {
302
308
  responseText: text,
303
309
  error,
304
310
  ...extractUsage(usage),
311
+ costNanos: extractGatewayCostNanos({ providerMetadata }),
305
312
  latencyMs: Date.now() - start,
306
313
  });
307
314
  };
@@ -310,8 +317,10 @@ export class AIBudget {
310
317
  if (chunk?.type === "text-delta") {
311
318
  text += chunk.delta ?? chunk.textDelta ?? "";
312
319
  }
313
- if (chunk?.type === "finish")
320
+ if (chunk?.type === "finish") {
314
321
  usage = chunk.usage;
322
+ providerMetadata = chunk.providerMetadata ?? providerMetadata;
323
+ }
315
324
  if (chunk?.type === "error")
316
325
  void settle(String(chunk.error));
317
326
  controller.enqueue(chunk);
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.8",
10
+ "version": "0.0.2-alpha.9",
11
11
  "license": "Apache-2.0",
12
12
  "type": "module",
13
13
  "keywords": [
@@ -174,20 +174,27 @@ function extractUsage(usage: any): {
174
174
  };
175
175
  }
176
176
 
177
- // The gateway doesn't report a dollar cost today, but if a future response ever
178
- // carries an unambiguous nanodollar cost we pass it straight through as
179
- // authoritative (finishRequest prefers it over the token-based estimate). Only
180
- // an explicitly nano-denominated field is trusted a bare `cost` could be in
181
- // dollars and silently mis-bill by 1e9×.
177
+ // The AI Gateway reports the authoritative dollar cost of each request.
178
+ // @convex-dev/ai-sdk-provider surfaces it at
179
+ // `providerMetadata.convexGateway.cost` (USD); convert to nanodollars and pass
180
+ // it through as authoritative (finishRequest prefers it over the token-based
181
+ // estimate). No-op on older provider versions that don't surface it.
182
182
  function extractGatewayCostNanos(result: any): number | undefined {
183
183
  const meta = result?.providerMetadata?.convexGateway;
184
- const candidates = [meta?.costNanos, result?.usage?.costNanos];
185
- for (const c of candidates) {
186
- if (typeof c === "number" && Number.isFinite(c) && c >= 0) return c;
184
+ const costUsd = meta?.cost;
185
+ if (typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd >= 0) {
186
+ return Math.round(costUsd * NANOS_PER_DOLLAR);
187
+ }
188
+ // Back-compat: honor an explicitly nano-denominated field if ever present.
189
+ const costNanos = meta?.costNanos ?? result?.usage?.costNanos;
190
+ if (typeof costNanos === "number" && Number.isFinite(costNanos) && costNanos >= 0) {
191
+ return Math.round(costNanos);
187
192
  }
188
193
  return undefined;
189
194
  }
190
195
 
196
+ const NANOS_PER_DOLLAR = 1e9;
197
+
191
198
  // Flatten an AI SDK prompt (roles + content parts) into simple storable messages.
192
199
  function simplifyPrompt(prompt: any): Message[] {
193
200
  if (!Array.isArray(prompt)) return [];
@@ -483,6 +490,7 @@ export class AIBudget {
483
490
  const start = Date.now();
484
491
  let text = "";
485
492
  let usage: any = undefined;
493
+ let providerMetadata: any = undefined;
486
494
  try {
487
495
  const result = await doStream();
488
496
  // finishRequest is idempotent (terminal-guarded server-side), so
@@ -498,6 +506,7 @@ export class AIBudget {
498
506
  responseText: text,
499
507
  error,
500
508
  ...extractUsage(usage),
509
+ costNanos: extractGatewayCostNanos({ providerMetadata }),
501
510
  latencyMs: Date.now() - start,
502
511
  });
503
512
  };
@@ -507,7 +516,10 @@ export class AIBudget {
507
516
  if (chunk?.type === "text-delta") {
508
517
  text += chunk.delta ?? chunk.textDelta ?? "";
509
518
  }
510
- if (chunk?.type === "finish") usage = chunk.usage;
519
+ if (chunk?.type === "finish") {
520
+ usage = chunk.usage;
521
+ providerMetadata = chunk.providerMetadata ?? providerMetadata;
522
+ }
511
523
  if (chunk?.type === "error") void settle(String(chunk.error));
512
524
  controller.enqueue(chunk);
513
525
  },