@x12i/ai-gateway 9.0.8 → 9.0.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.
@@ -36,3 +36,9 @@ export declare function extractTokenUsageFromRouterResponse(routerResponse: unkn
36
36
  completion: number;
37
37
  total: number;
38
38
  };
39
+ /**
40
+ * Best-effort USD cost from router/sync AIResponse shape: metadata.costUsd (preferred),
41
+ * metadata.attempts[].costUsd, response root, then common raw payload locations.
42
+ * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
43
+ */
44
+ export declare function extractCostUsdFromRouterResponse(routerResponse: unknown): number | undefined;
@@ -231,3 +231,50 @@ export function extractTokenUsageFromRouterResponse(routerResponse) {
231
231
  }
232
232
  return { prompt: 0, completion: 0, total: 0 };
233
233
  }
234
+ /**
235
+ * Best-effort USD cost from router/sync AIResponse shape: metadata.costUsd (preferred),
236
+ * metadata.attempts[].costUsd, response root, then common raw payload locations.
237
+ * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
238
+ */
239
+ export function extractCostUsdFromRouterResponse(routerResponse) {
240
+ if (routerResponse == null || typeof routerResponse !== 'object')
241
+ return undefined;
242
+ const r = routerResponse;
243
+ const meta = r.metadata != null && typeof r.metadata === 'object'
244
+ ? r.metadata
245
+ : undefined;
246
+ const pick = (...vals) => firstFiniteNumber(...vals);
247
+ const fromMeta = pick(meta?.costUsd, meta?.cost);
248
+ if (fromMeta !== undefined)
249
+ return fromMeta;
250
+ const fromRoot = pick(r.costUsd, r.cost);
251
+ if (fromRoot !== undefined)
252
+ return fromRoot;
253
+ const attempts = meta?.attempts;
254
+ if (Array.isArray(attempts)) {
255
+ for (let i = attempts.length - 1; i >= 0; i--) {
256
+ const a = attempts[i];
257
+ if (a != null && typeof a === 'object') {
258
+ const o = a;
259
+ const c = pick(o.costUsd, o.cost);
260
+ if (c !== undefined)
261
+ return c;
262
+ }
263
+ }
264
+ }
265
+ const raw = r.rawResponse ?? r.raw;
266
+ if (raw != null && typeof raw === 'object') {
267
+ const rawObj = raw;
268
+ const usage = rawObj.usage;
269
+ if (usage != null && typeof usage === 'object') {
270
+ const u = usage;
271
+ const fromUsage = pick(u.cost, u.costUsd, u.total_cost, u.totalCost);
272
+ if (fromUsage !== undefined)
273
+ return fromUsage;
274
+ }
275
+ const fromRawTop = pick(rawObj.cost, rawObj.costUsd);
276
+ if (fromRawTop !== undefined)
277
+ return fromRawTop;
278
+ }
279
+ return undefined;
280
+ }
package/dist/gateway.js CHANGED
@@ -8,7 +8,7 @@ import { ensureGatewayRequestIdentity } from './activity-manager.js';
8
8
  import { initializeGatewayComponents } from './gateway-config.js';
9
9
  import { buildMessages } from './message-builder.js';
10
10
  import { extractJsonFromFlexMd } from './flex-md-loader.js';
11
- import { extractTokenUsageFromRouterResponse, mergeConfig } from './gateway-utils.js';
11
+ import { extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig } from './gateway-utils.js';
12
12
  import { autoRegisterProviders } from './gateway-provider-auto-register.js';
13
13
  import { setGatewayLastJobId, setGatewayRuntimeClients } from './runtime-objects.js';
14
14
  import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
@@ -16,6 +16,25 @@ import { invokeWithRetry } from './gateway-retry.js';
16
16
  /** Error message thrown by the router when no provider is registered or specified */
17
17
  const NO_PROVIDER_ERROR = 'No provider specified and no providers registered';
18
18
  const NO_PROVIDER_HINT = ' Set OPEN_ROUTER_KEY (or OPENROUTER_API_KEY) in the environment to use OpenRouter, or register a provider with the router (e.g. via autoRegisterProviders or gateway config).';
19
+ /** Warn when a successful call reports no tokens and/or explicit zero cost (often missing adapter metadata). */
20
+ function warnIfSuccessfulInvokeReportsZeroUsageOrCost(logger, identity, meta, invokeKind) {
21
+ const { tokens, costUsd, cost } = meta;
22
+ const zeroTokens = tokens.prompt === 0 && tokens.completion === 0 && tokens.total === 0;
23
+ const zeroCostUsd = typeof costUsd === 'number' && costUsd === 0;
24
+ const zeroCost = typeof cost === 'number' && cost === 0;
25
+ if (!zeroTokens && !zeroCostUsd && !zeroCost)
26
+ return;
27
+ logger.warn('Successful provider response reported zero token usage and/or zero cost; verify router adapter usage and billing metadata', withActivityIdentity(identity, {
28
+ invokeKind,
29
+ zeroTokens,
30
+ zeroCostUsd,
31
+ zeroCostField: zeroCost,
32
+ tokens,
33
+ costUsd,
34
+ cost,
35
+ debugKind: gatewayLogDebug.anomaly
36
+ }));
37
+ }
19
38
  /**
20
39
  * Simplified AI Gateway - Clean proxy implementation
21
40
  */
@@ -87,6 +106,8 @@ export class AIGateway {
87
106
  },
88
107
  mode: 'sync'
89
108
  });
109
+ const costUsdChat = extractCostUsdFromRouterResponse(response);
110
+ const metaChat = response?.metadata || {};
90
111
  // Create enhanced response
91
112
  const enhancedResponse = {
92
113
  content: response.content || '',
@@ -96,13 +117,20 @@ export class AIGateway {
96
117
  latencyMs: Date.now() - startTime,
97
118
  tokens: extractTokenUsageFromRouterResponse(response),
98
119
  taskTypeId,
99
- agentType: 'chat'
120
+ agentType: 'chat',
121
+ ...(typeof costUsdChat === 'number'
122
+ ? {
123
+ costUsd: costUsdChat,
124
+ ...(typeof metaChat.cost === 'number' ? { cost: metaChat.cost } : { cost: costUsdChat })
125
+ }
126
+ : {})
100
127
  }
101
128
  };
102
129
  // Track activity success if activity was started
103
130
  if (activity) {
104
131
  try {
105
132
  await this.activityManager.logSuccess(activity, {
133
+ ...(typeof costUsdChat === 'number' ? { cost: costUsdChat } : {}),
106
134
  response: enhancedResponse,
107
135
  endTime: Date.now(),
108
136
  duration: Date.now() - startTime
@@ -116,6 +144,11 @@ export class AIGateway {
116
144
  });
117
145
  }
118
146
  }
147
+ warnIfSuccessfulInvokeReportsZeroUsageOrCost(this.logger, request.identity, {
148
+ tokens: enhancedResponse.metadata.tokens,
149
+ costUsd: enhancedResponse.metadata.costUsd,
150
+ cost: enhancedResponse.metadata.cost
151
+ }, 'invokeChat');
119
152
  return enhancedResponse;
120
153
  }
121
154
  catch (error) {
@@ -369,17 +402,9 @@ export class AIGateway {
369
402
  a.routing.requestIds = requestIds;
370
403
  a.modelUsed =
371
404
  meta?.modelUsed || meta?.model || respAny.model || candidate.model;
372
- const costUsd = typeof meta?.costUsd === 'number'
373
- ? meta.costUsd
374
- : typeof meta?.cost === 'number'
375
- ? meta.cost
376
- : typeof respAny?.costUsd === 'number'
377
- ? respAny.costUsd
378
- : typeof respAny?.cost === 'number'
379
- ? respAny.cost
380
- : undefined;
381
- if (typeof costUsd === 'number')
382
- a.costUsd = costUsd;
405
+ const attemptCostUsd = extractCostUsdFromRouterResponse(respAny);
406
+ if (typeof attemptCostUsd === 'number')
407
+ a.costUsd = attemptCostUsd;
383
408
  if (includeRawProviderPayload) {
384
409
  // Size-capped preview only.
385
410
  const raw = respAny.rawResponse ?? respAny.raw ?? respAny;
@@ -489,6 +514,8 @@ export class AIGateway {
489
514
  contentType = 'structured';
490
515
  parsingMethod = 'flex-md';
491
516
  const tokens = extractTokenUsageFromRouterResponse(routerResponse);
517
+ const resolvedCostUsd = extractCostUsdFromRouterResponse(routerResponse);
518
+ const routerMetaForCost = routerResponse?.metadata || {};
492
519
  const enhancedResponse = {
493
520
  content: content,
494
521
  parsedContent: parsedContent,
@@ -501,6 +528,14 @@ export class AIGateway {
501
528
  agentType: 'ai',
502
529
  contentType,
503
530
  parsingMethod,
531
+ ...(typeof resolvedCostUsd === 'number'
532
+ ? {
533
+ costUsd: resolvedCostUsd,
534
+ ...(typeof routerMetaForCost.cost === 'number'
535
+ ? { cost: routerMetaForCost.cost }
536
+ : { cost: resolvedCostUsd })
537
+ }
538
+ : {}),
504
539
  ...(traceEnabled
505
540
  ? (() => {
506
541
  const meta = routerResponse?.metadata || {};
@@ -512,18 +547,11 @@ export class AIGateway {
512
547
  : typeof mergedConfig?.maxTokens === 'number'
513
548
  ? mergedConfig.maxTokens
514
549
  : undefined;
515
- const costUsd = typeof meta.costUsd === 'number'
516
- ? meta.costUsd
517
- : typeof meta.cost === 'number'
518
- ? meta.cost
519
- : undefined;
520
550
  return {
521
551
  provider,
522
552
  region,
523
553
  modelUsed,
524
554
  maxTokensRequested,
525
- cost: typeof meta.cost === 'number' ? meta.cost : undefined,
526
- costUsd,
527
555
  requestIds: traceRequestIds,
528
556
  retryCount: traceRetryCount,
529
557
  fallbackCount: traceFallbackCount,
@@ -549,6 +577,7 @@ export class AIGateway {
549
577
  usage: tokens
550
578
  };
551
579
  await this.activityManager.logSuccess(activity, {
580
+ ...(typeof resolvedCostUsd === 'number' ? { cost: resolvedCostUsd } : {}),
552
581
  response: activityResponse,
553
582
  endTime: Date.now(),
554
583
  duration: Date.now() - startTime
@@ -562,6 +591,11 @@ export class AIGateway {
562
591
  });
563
592
  }
564
593
  }
594
+ warnIfSuccessfulInvokeReportsZeroUsageOrCost(this.logger, request.identity, {
595
+ tokens: enhancedResponse.metadata.tokens,
596
+ costUsd: enhancedResponse.metadata.costUsd,
597
+ cost: enhancedResponse.metadata.cost
598
+ }, 'invoke');
565
599
  this.logger.debug('gateway: enhancedResponse', withActivityIdentity(request.identity, {
566
600
  latencyMs: enhancedResponse.metadata?.latencyMs,
567
601
  contentType: enhancedResponse.metadata?.contentType,
@@ -42,6 +42,7 @@ exports.ensureTaskTypeId = ensureTaskTypeId;
42
42
  exports.mergeConfig = mergeConfig;
43
43
  exports.normalizeRouterUsageTokens = normalizeRouterUsageTokens;
44
44
  exports.extractTokenUsageFromRouterResponse = extractTokenUsageFromRouterResponse;
45
+ exports.extractCostUsdFromRouterResponse = extractCostUsdFromRouterResponse;
45
46
  const crypto = __importStar(require("crypto"));
46
47
  const gateway_instructions_js_1 = require("./gateway-instructions.cjs");
47
48
  const flex_md_loader_js_1 = require("./flex-md-loader.cjs");
@@ -271,3 +272,50 @@ function extractTokenUsageFromRouterResponse(routerResponse) {
271
272
  }
272
273
  return { prompt: 0, completion: 0, total: 0 };
273
274
  }
275
+ /**
276
+ * Best-effort USD cost from router/sync AIResponse shape: metadata.costUsd (preferred),
277
+ * metadata.attempts[].costUsd, response root, then common raw payload locations.
278
+ * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
279
+ */
280
+ function extractCostUsdFromRouterResponse(routerResponse) {
281
+ if (routerResponse == null || typeof routerResponse !== 'object')
282
+ return undefined;
283
+ const r = routerResponse;
284
+ const meta = r.metadata != null && typeof r.metadata === 'object'
285
+ ? r.metadata
286
+ : undefined;
287
+ const pick = (...vals) => firstFiniteNumber(...vals);
288
+ const fromMeta = pick(meta?.costUsd, meta?.cost);
289
+ if (fromMeta !== undefined)
290
+ return fromMeta;
291
+ const fromRoot = pick(r.costUsd, r.cost);
292
+ if (fromRoot !== undefined)
293
+ return fromRoot;
294
+ const attempts = meta?.attempts;
295
+ if (Array.isArray(attempts)) {
296
+ for (let i = attempts.length - 1; i >= 0; i--) {
297
+ const a = attempts[i];
298
+ if (a != null && typeof a === 'object') {
299
+ const o = a;
300
+ const c = pick(o.costUsd, o.cost);
301
+ if (c !== undefined)
302
+ return c;
303
+ }
304
+ }
305
+ }
306
+ const raw = r.rawResponse ?? r.raw;
307
+ if (raw != null && typeof raw === 'object') {
308
+ const rawObj = raw;
309
+ const usage = rawObj.usage;
310
+ if (usage != null && typeof usage === 'object') {
311
+ const u = usage;
312
+ const fromUsage = pick(u.cost, u.costUsd, u.total_cost, u.totalCost);
313
+ if (fromUsage !== undefined)
314
+ return fromUsage;
315
+ }
316
+ const fromRawTop = pick(rawObj.cost, rawObj.costUsd);
317
+ if (fromRawTop !== undefined)
318
+ return fromRawTop;
319
+ }
320
+ return undefined;
321
+ }
@@ -36,3 +36,9 @@ export declare function extractTokenUsageFromRouterResponse(routerResponse: unkn
36
36
  completion: number;
37
37
  total: number;
38
38
  };
39
+ /**
40
+ * Best-effort USD cost from router/sync AIResponse shape: metadata.costUsd (preferred),
41
+ * metadata.attempts[].costUsd, response root, then common raw payload locations.
42
+ * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
43
+ */
44
+ export declare function extractCostUsdFromRouterResponse(routerResponse: unknown): number | undefined;
@@ -19,6 +19,25 @@ const gateway_retry_js_1 = require("./gateway-retry.cjs");
19
19
  /** Error message thrown by the router when no provider is registered or specified */
20
20
  const NO_PROVIDER_ERROR = 'No provider specified and no providers registered';
21
21
  const NO_PROVIDER_HINT = ' Set OPEN_ROUTER_KEY (or OPENROUTER_API_KEY) in the environment to use OpenRouter, or register a provider with the router (e.g. via autoRegisterProviders or gateway config).';
22
+ /** Warn when a successful call reports no tokens and/or explicit zero cost (often missing adapter metadata). */
23
+ function warnIfSuccessfulInvokeReportsZeroUsageOrCost(logger, identity, meta, invokeKind) {
24
+ const { tokens, costUsd, cost } = meta;
25
+ const zeroTokens = tokens.prompt === 0 && tokens.completion === 0 && tokens.total === 0;
26
+ const zeroCostUsd = typeof costUsd === 'number' && costUsd === 0;
27
+ const zeroCost = typeof cost === 'number' && cost === 0;
28
+ if (!zeroTokens && !zeroCostUsd && !zeroCost)
29
+ return;
30
+ logger.warn('Successful provider response reported zero token usage and/or zero cost; verify router adapter usage and billing metadata', (0, gateway_log_meta_js_1.withActivityIdentity)(identity, {
31
+ invokeKind,
32
+ zeroTokens,
33
+ zeroCostUsd,
34
+ zeroCostField: zeroCost,
35
+ tokens,
36
+ costUsd,
37
+ cost,
38
+ debugKind: gateway_log_meta_js_1.gatewayLogDebug.anomaly
39
+ }));
40
+ }
22
41
  /**
23
42
  * Simplified AI Gateway - Clean proxy implementation
24
43
  */
@@ -90,6 +109,8 @@ class AIGateway {
90
109
  },
91
110
  mode: 'sync'
92
111
  });
112
+ const costUsdChat = (0, gateway_utils_js_1.extractCostUsdFromRouterResponse)(response);
113
+ const metaChat = response?.metadata || {};
93
114
  // Create enhanced response
94
115
  const enhancedResponse = {
95
116
  content: response.content || '',
@@ -99,13 +120,20 @@ class AIGateway {
99
120
  latencyMs: Date.now() - startTime,
100
121
  tokens: (0, gateway_utils_js_1.extractTokenUsageFromRouterResponse)(response),
101
122
  taskTypeId,
102
- agentType: 'chat'
123
+ agentType: 'chat',
124
+ ...(typeof costUsdChat === 'number'
125
+ ? {
126
+ costUsd: costUsdChat,
127
+ ...(typeof metaChat.cost === 'number' ? { cost: metaChat.cost } : { cost: costUsdChat })
128
+ }
129
+ : {})
103
130
  }
104
131
  };
105
132
  // Track activity success if activity was started
106
133
  if (activity) {
107
134
  try {
108
135
  await this.activityManager.logSuccess(activity, {
136
+ ...(typeof costUsdChat === 'number' ? { cost: costUsdChat } : {}),
109
137
  response: enhancedResponse,
110
138
  endTime: Date.now(),
111
139
  duration: Date.now() - startTime
@@ -119,6 +147,11 @@ class AIGateway {
119
147
  });
120
148
  }
121
149
  }
150
+ warnIfSuccessfulInvokeReportsZeroUsageOrCost(this.logger, request.identity, {
151
+ tokens: enhancedResponse.metadata.tokens,
152
+ costUsd: enhancedResponse.metadata.costUsd,
153
+ cost: enhancedResponse.metadata.cost
154
+ }, 'invokeChat');
122
155
  return enhancedResponse;
123
156
  }
124
157
  catch (error) {
@@ -372,17 +405,9 @@ class AIGateway {
372
405
  a.routing.requestIds = requestIds;
373
406
  a.modelUsed =
374
407
  meta?.modelUsed || meta?.model || respAny.model || candidate.model;
375
- const costUsd = typeof meta?.costUsd === 'number'
376
- ? meta.costUsd
377
- : typeof meta?.cost === 'number'
378
- ? meta.cost
379
- : typeof respAny?.costUsd === 'number'
380
- ? respAny.costUsd
381
- : typeof respAny?.cost === 'number'
382
- ? respAny.cost
383
- : undefined;
384
- if (typeof costUsd === 'number')
385
- a.costUsd = costUsd;
408
+ const attemptCostUsd = (0, gateway_utils_js_1.extractCostUsdFromRouterResponse)(respAny);
409
+ if (typeof attemptCostUsd === 'number')
410
+ a.costUsd = attemptCostUsd;
386
411
  if (includeRawProviderPayload) {
387
412
  // Size-capped preview only.
388
413
  const raw = respAny.rawResponse ?? respAny.raw ?? respAny;
@@ -492,6 +517,8 @@ class AIGateway {
492
517
  contentType = 'structured';
493
518
  parsingMethod = 'flex-md';
494
519
  const tokens = (0, gateway_utils_js_1.extractTokenUsageFromRouterResponse)(routerResponse);
520
+ const resolvedCostUsd = (0, gateway_utils_js_1.extractCostUsdFromRouterResponse)(routerResponse);
521
+ const routerMetaForCost = routerResponse?.metadata || {};
495
522
  const enhancedResponse = {
496
523
  content: content,
497
524
  parsedContent: parsedContent,
@@ -504,6 +531,14 @@ class AIGateway {
504
531
  agentType: 'ai',
505
532
  contentType,
506
533
  parsingMethod,
534
+ ...(typeof resolvedCostUsd === 'number'
535
+ ? {
536
+ costUsd: resolvedCostUsd,
537
+ ...(typeof routerMetaForCost.cost === 'number'
538
+ ? { cost: routerMetaForCost.cost }
539
+ : { cost: resolvedCostUsd })
540
+ }
541
+ : {}),
507
542
  ...(traceEnabled
508
543
  ? (() => {
509
544
  const meta = routerResponse?.metadata || {};
@@ -515,18 +550,11 @@ class AIGateway {
515
550
  : typeof mergedConfig?.maxTokens === 'number'
516
551
  ? mergedConfig.maxTokens
517
552
  : undefined;
518
- const costUsd = typeof meta.costUsd === 'number'
519
- ? meta.costUsd
520
- : typeof meta.cost === 'number'
521
- ? meta.cost
522
- : undefined;
523
553
  return {
524
554
  provider,
525
555
  region,
526
556
  modelUsed,
527
557
  maxTokensRequested,
528
- cost: typeof meta.cost === 'number' ? meta.cost : undefined,
529
- costUsd,
530
558
  requestIds: traceRequestIds,
531
559
  retryCount: traceRetryCount,
532
560
  fallbackCount: traceFallbackCount,
@@ -552,6 +580,7 @@ class AIGateway {
552
580
  usage: tokens
553
581
  };
554
582
  await this.activityManager.logSuccess(activity, {
583
+ ...(typeof resolvedCostUsd === 'number' ? { cost: resolvedCostUsd } : {}),
555
584
  response: activityResponse,
556
585
  endTime: Date.now(),
557
586
  duration: Date.now() - startTime
@@ -565,6 +594,11 @@ class AIGateway {
565
594
  });
566
595
  }
567
596
  }
597
+ warnIfSuccessfulInvokeReportsZeroUsageOrCost(this.logger, request.identity, {
598
+ tokens: enhancedResponse.metadata.tokens,
599
+ costUsd: enhancedResponse.metadata.costUsd,
600
+ cost: enhancedResponse.metadata.cost
601
+ }, 'invoke');
568
602
  this.logger.debug('gateway: enhancedResponse', (0, gateway_log_meta_js_1.withActivityIdentity)(request.identity, {
569
603
  latencyMs: enhancedResponse.metadata?.latencyMs,
570
604
  contentType: enhancedResponse.metadata?.contentType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-gateway",
3
- "version": "9.0.8",
3
+ "version": "9.0.9",
4
4
  "description": "AI Gateway - Unified interface for LLM provider routing and management",
5
5
  "type": "module",
6
6
  "exports": {