@x12i/ai-gateway 9.3.4 → 9.4.0

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.
@@ -4,7 +4,7 @@
4
4
  * Manages activity tracking for LLM requests.
5
5
  * Wraps the ActivityTracker and provides convenience methods.
6
6
  */
7
- import { Activix } from '@x12i/activix';
7
+ import { Activix, type ActivixAutoCostOptions } from '@x12i/activix';
8
8
  import type { Logxer } from '@x12i/logxer';
9
9
  import type { ActivityIdentity, ChatRequest, AIRequest, FailureType, LLMResponseFailureSubtype, ResponseParsingFailureSubtype } from './types.js';
10
10
  type Request = ChatRequest | AIRequest;
@@ -31,6 +31,11 @@ export interface ActivityManagerConfig {
31
31
  enableActivityTracking: boolean;
32
32
  customTracker?: Activix;
33
33
  logger: Logxer;
34
+ /**
35
+ * Activix 7.2+ {@link ActivixAutoCostOptions}: fill `outer.cost` via @x12i/ai-tools when the gateway
36
+ * did not supply a valid cost. Ignored when `customTracker` is provided.
37
+ */
38
+ autoCost?: boolean | ActivixAutoCostOptions;
34
39
  }
35
40
  /**
36
41
  * Manages activity tracking lifecycle
@@ -4,7 +4,7 @@
4
4
  * Manages activity tracking for LLM requests.
5
5
  * Wraps the ActivityTracker and provides convenience methods.
6
6
  */
7
- import { Activix, activixActivityIo, activixOuterTier, resolveActivixLogsDatabaseName, resolveActivixMongoUriFromEnv } from '@x12i/activix';
7
+ import { Activix, activixActivityIo, activixOuterTier, normalizeToActivixCostShape, resolveActivixLogsDatabaseName, resolveActivixMongoUriFromEnv } from '@x12i/activix';
8
8
  import { resolveActivityTrackingConfig } from './config/activity-tracking-config.js';
9
9
  import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
10
10
  function readAiRequestIdFromRequest(request) {
@@ -165,16 +165,11 @@ function pickActivixUsageTokens(response) {
165
165
  };
166
166
  }
167
167
  /**
168
- * Activix v6+ `outer.cost` from gateway billing + routing metadata (Run Analysis G8).
168
+ * Activix 7.x `outer.cost` from gateway billing + routing (Run Analysis G8).
169
+ * Uses Activix {@link normalizeToActivixCostShape} so the shape matches package validators.
169
170
  */
170
171
  function buildActivixOuterCost(routingMeta, billing, response) {
171
- const usd = typeof billing.cost === 'number' && Number.isFinite(billing.cost)
172
- ? billing.cost
173
- : typeof routingMeta.costUsd === 'number' && Number.isFinite(routingMeta.costUsd)
174
- ? routingMeta.costUsd
175
- : typeof routingMeta.cost === 'number' && Number.isFinite(routingMeta.cost)
176
- ? routingMeta.cost
177
- : undefined;
172
+ const usd = typeof billing.cost === 'number' && Number.isFinite(billing.cost) ? billing.cost : undefined;
178
173
  const tokens = pickActivixUsageTokens(response);
179
174
  const provider = typeof routingMeta.provider === 'string' ? routingMeta.provider : undefined;
180
175
  const model = typeof routingMeta.modelUsed === 'string'
@@ -189,20 +184,34 @@ function buildActivixOuterCost(routingMeta, billing, response) {
189
184
  if (billing.costBreakdown != null && typeof billing.costBreakdown === 'object') {
190
185
  details.costBreakdown = billing.costBreakdown;
191
186
  }
192
- const hasDetails = Object.keys(details).length > 0;
193
- if (usd === undefined && !tokens && !provider && !model && !hasDetails) {
194
- return undefined;
195
- }
196
- return {
187
+ const candidate = {
197
188
  ...(usd !== undefined ? { usd, unit: 'USD' } : {}),
198
189
  ...(tokens ? { tokens } : {}),
199
190
  ...(provider ? { provider } : {}),
200
191
  ...(model ? { model } : {}),
201
- ...(hasDetails ? { details } : {})
192
+ ...(Object.keys(details).length > 0 ? { details } : {})
193
+ };
194
+ return normalizeToActivixCostShape(candidate, 'gateway.outer.cost') ?? undefined;
195
+ }
196
+ /** Run-level record metadata (Activix 7.x top-level `metadata`, sibling to `outer`). */
197
+ function buildActivixRecordMetadata(response, billing) {
198
+ const out = {
199
+ ...pickActivixCompletionRoutingMetadata(response)
202
200
  };
201
+ if (billing.costStatus === 'priced' || billing.costStatus === 'unpriced') {
202
+ out.costStatus = billing.costStatus;
203
+ }
204
+ if (typeof billing.cost === 'number' && Number.isFinite(billing.cost)) {
205
+ out.cost = billing.cost;
206
+ out.costUsd = billing.cost;
207
+ }
208
+ if (billing.costBreakdown != null && typeof billing.costBreakdown === 'object') {
209
+ out.costBreakdown = billing.costBreakdown;
210
+ }
211
+ return out;
203
212
  }
204
- /** Routing / generation facts for Activix `outer.metadata` on completion (includes billing mirror). */
205
- function pickActivixCompletionRoutingMetadata(response, billing) {
213
+ /** Routing / generation facts for Activix `outer.metadata` on completion (no billing — see root + `outer.cost`). */
214
+ function pickActivixCompletionRoutingMetadata(response) {
206
215
  const out = {};
207
216
  if (response != null && typeof response === 'object') {
208
217
  const meta = response.metadata;
@@ -221,30 +230,6 @@ function pickActivixCompletionRoutingMetadata(response, billing) {
221
230
  if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
222
231
  out.effectiveModelConfig = m.effectiveModelConfig;
223
232
  }
224
- if (typeof m.cost === 'number' && Number.isFinite(m.cost))
225
- out.cost = m.cost;
226
- if (typeof m.costUsd === 'number' && Number.isFinite(m.costUsd))
227
- out.costUsd = m.costUsd;
228
- if (m.costStatus === 'priced' || m.costStatus === 'unpriced')
229
- out.costStatus = m.costStatus;
230
- if (m.costBreakdown != null && typeof m.costBreakdown === 'object') {
231
- out.costBreakdown = m.costBreakdown;
232
- }
233
- }
234
- }
235
- if (billing) {
236
- if ((out.costStatus !== 'priced' && out.costStatus !== 'unpriced') &&
237
- (billing.costStatus === 'priced' || billing.costStatus === 'unpriced')) {
238
- out.costStatus = billing.costStatus;
239
- }
240
- if (typeof billing.cost === 'number' && Number.isFinite(billing.cost)) {
241
- if (out.cost === undefined)
242
- out.cost = billing.cost;
243
- if (out.costUsd === undefined)
244
- out.costUsd = billing.cost;
245
- }
246
- if (out.costBreakdown === undefined && billing.costBreakdown != null) {
247
- out.costBreakdown = billing.costBreakdown;
248
233
  }
249
234
  }
250
235
  return out;
@@ -345,8 +330,7 @@ export class ActivityManager {
345
330
  failed: 'failed',
346
331
  timeout: 'timeout'
347
332
  };
348
- this.activix = config.customTracker ?? new Activix({
349
- // Keep mode explicit for operational clarity (matches integration checklist expectations).
333
+ const activixOptions = {
350
334
  storageMode: 'automatic',
351
335
  collections: [
352
336
  {
@@ -385,7 +369,14 @@ export class ActivityManager {
385
369
  exponentialBackoff: false
386
370
  }
387
371
  }
388
- });
372
+ };
373
+ if (config.autoCost !== undefined && config.autoCost !== false) {
374
+ activixOptions.autoCost =
375
+ config.autoCost === true
376
+ ? { enabled: true, overwriteOuterCost: false }
377
+ : { enabled: true, overwriteOuterCost: false, ...config.autoCost };
378
+ }
379
+ this.activix = config.customTracker ?? new Activix(activixOptions);
389
380
  this.initPromise = this.activix
390
381
  .init()
391
382
  .then(() => {
@@ -939,8 +930,9 @@ export class ActivityManager {
939
930
  costStatus: details.costStatus,
940
931
  costBreakdown: details.costBreakdown
941
932
  };
942
- const outerMetadata = pickActivixCompletionRoutingMetadata(details.response, billingSlice);
933
+ const outerMetadata = pickActivixCompletionRoutingMetadata(details.response);
943
934
  const outerCost = buildActivixOuterCost(outerMetadata, billingSlice, details.response);
935
+ const recordMetadata = buildActivixRecordMetadata(details.response, billingSlice);
944
936
  await this.activix.completeRecord(activity.activityId, {
945
937
  cost: details.cost,
946
938
  ...(typeof details.cost === 'number' && Number.isFinite(details.cost)
@@ -948,13 +940,12 @@ export class ActivityManager {
948
940
  : {}),
949
941
  ...(details.costStatus ? { costStatus: details.costStatus } : {}),
950
942
  response: details.response,
943
+ ...(Object.keys(recordMetadata).length > 0 ? { metadata: recordMetadata } : {}),
951
944
  outer: {
952
945
  output: details.response,
953
946
  metadata: outerMetadata,
954
947
  ...(outerCost ? { cost: outerCost } : {})
955
- },
956
- endTime: details.endTime,
957
- duration: details.duration
948
+ }
958
949
  }, { collection });
959
950
  this.logger.debug('Activix.completeRecord completed', {
960
951
  aiRequestId: activity.aiRequestId,
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Lazy @x12i/ai-tools catalog + cost calculator bootstrap.
3
3
  */
4
- import { AiModelsCatalogClient, CostCalculator, ensureAiModelsCatalog } from '@x12i/ai-tools';
4
+ import { AiModelsCatalogClient, CostCalculator } from '@x12i/ai-tools';
5
5
  import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
6
6
  let sharedClientPromise = null;
7
7
  let sharedConfigKey;
8
8
  let bootstrapFailedLogged = false;
9
9
  function configKey(config) {
10
- const injected = config.aiTools?.catalox ? 'injected' : 'env';
11
- return `${injected}:${config.aiTools?.cacheTtlMs ?? ''}:${config.aiTools?.costIncludeBreakdown ?? ''}`;
10
+ return `${config.aiTools?.cacheTtlMs ?? ''}:${config.aiTools?.costIncludeBreakdown ?? ''}:${config.aiTools?.bundledOnly ?? ''}`;
12
11
  }
13
12
  /**
14
13
  * Returns catalog + calculator, or null when disabled or bootstrap fails.
@@ -35,16 +34,9 @@ export function resetAiToolsClientForTests() {
35
34
  }
36
35
  async function bootstrapAiTools(config, logger) {
37
36
  try {
38
- let catalox = config.aiTools?.catalox;
39
- if (!catalox) {
40
- const { createCataloxFromEnv } = await import('@x12i/catalox/firebase');
41
- const bootstrapped = createCataloxFromEnv();
42
- catalox = bootstrapped.catalox;
43
- }
44
- await ensureAiModelsCatalog(catalox);
45
37
  const catalog = new AiModelsCatalogClient({
46
- catalox,
47
- cacheTtlMs: config.aiTools?.cacheTtlMs
38
+ cacheTtlMs: config.aiTools?.cacheTtlMs,
39
+ ...(config.aiTools?.bundledOnly ? { bundledOnly: true } : {})
48
40
  });
49
41
  const calculator = new CostCalculator(catalog, {
50
42
  includeBreakdown: config.aiTools?.costIncludeBreakdown === true
@@ -265,7 +265,18 @@ export function initializeGatewayComponents(config) {
265
265
  const activityManager = new ActivityManager({
266
266
  enableActivityTracking: config.enableActivityTracking ?? true,
267
267
  customTracker: config.activityTracker,
268
- logger
268
+ logger,
269
+ ...(config.activityTracker
270
+ ? {}
271
+ : {
272
+ autoCost: config.aiTools?.enabled === false || config.aiTools?.calculateCost === false
273
+ ? false
274
+ : {
275
+ enabled: true,
276
+ overwriteOuterCost: false,
277
+ ...(config.aiTools?.bundledOnly ? { bundledOnly: true } : {})
278
+ }
279
+ })
269
280
  });
270
281
  const templateRendering = mergeTemplateRenderOptions(defaultTemplateRendering, config.templateRendering);
271
282
  const instructionsBlockOverrides = {
@@ -2,9 +2,9 @@
2
2
  * Gateway Utilities Module
3
3
  * Handles utility functions
4
4
  */
5
- import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceMergedConfig, GatewayTraceRequestIds, ModelConfig } from './types.js';
5
+ import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceAttempt, GatewayTraceMergedConfig, GatewayTraceRequestIds, GatewayTraceUsageSummary, ModelConfig } from './types.js';
6
6
  import type { Logxer } from '@x12i/logxer';
7
- import { type AiModelsCatalogClient, type CostCalculator } from '@x12i/ai-tools';
7
+ import { type AiCostResult, type AiModelsCatalogClient, type CostCalculator } from '@x12i/ai-tools';
8
8
  /**
9
9
  * Generates MD5 hash of a string
10
10
  */
@@ -91,6 +91,13 @@ export type ResolveCostCompletionOptions = {
91
91
  calculator?: CostCalculator | null;
92
92
  calculateCost?: boolean;
93
93
  };
94
+ /** Record shape for {@link CostCalculator.calculateFromRecord} (router + merged config + usage). */
95
+ export declare function buildGatewayPricingRecord(routerResponse: unknown, tokens: {
96
+ prompt: number;
97
+ completion: number;
98
+ total: number;
99
+ }, mergedConfig?: unknown): Record<string, unknown>;
100
+ export declare function mapAiCostResultToResolvedActivityCost(base: ResolvedActivityCost, result: AiCostResult): ResolvedActivityCost;
94
101
  /**
95
102
  * Router cost passthrough, then optional @x12i/ai-tools catalog pricing when still unpriced.
96
103
  */
@@ -99,6 +106,19 @@ export declare function resolveCostCompletionWithAiTools(routerResponse: unknown
99
106
  completion: number;
100
107
  total: number;
101
108
  }, options?: ResolveCostCompletionOptions): Promise<ResolvedActivityCost>;
109
+ /**
110
+ * Trace-mode summary: final token usage + resolved billing (after catalog pricing when applicable).
111
+ */
112
+ export declare function buildTraceUsageSummary(tokens: {
113
+ prompt: number;
114
+ completion: number;
115
+ total: number;
116
+ }, billing: ResolvedActivityCost, maxTokensRequested?: number): GatewayTraceUsageSummary | undefined;
117
+ /**
118
+ * Apply resolved billing to trace attempts: final successful attempt gets aggregate billing;
119
+ * other successful attempts without router cost get per-attempt catalog pricing when enabled.
120
+ */
121
+ export declare function enrichTraceAttemptsWithBilling(attempts: GatewayTraceAttempt[], finalBilling: ResolvedActivityCost, options?: ResolveCostCompletionOptions): Promise<void>;
102
122
  /**
103
123
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
104
124
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
@@ -434,6 +434,50 @@ export function resolveCostCompletionForActivity(routerResponse, tokens) {
434
434
  }
435
435
  return resolveActivityCostCompletion(tokens, costUsd);
436
436
  }
437
+ /** Record shape for {@link CostCalculator.calculateFromRecord} (router + merged config + usage). */
438
+ export function buildGatewayPricingRecord(routerResponse, tokens, mergedConfig) {
439
+ const base = routerResponse != null && typeof routerResponse === 'object'
440
+ ? { ...routerResponse }
441
+ : {};
442
+ const meta = base.metadata != null && typeof base.metadata === 'object'
443
+ ? { ...base.metadata }
444
+ : {};
445
+ const routing = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
446
+ return {
447
+ ...base,
448
+ usage: {
449
+ promptTokens: tokens.prompt,
450
+ completionTokens: tokens.completion,
451
+ totalTokens: tokens.total
452
+ },
453
+ tokens,
454
+ metadata: {
455
+ ...meta,
456
+ tokens,
457
+ ...(routing.provider ? { provider: routing.provider } : {}),
458
+ ...(routing.modelUsed
459
+ ? { modelUsed: routing.modelUsed, model: routing.modelUsed }
460
+ : {})
461
+ },
462
+ ...(mergedConfig != null ? { config: mergedConfig } : {})
463
+ };
464
+ }
465
+ export function mapAiCostResultToResolvedActivityCost(base, result) {
466
+ if (result.unknownModel) {
467
+ return base.costStatus ? base : { ...base, costStatus: 'unpriced' };
468
+ }
469
+ if (typeof result.cost !== 'number' || !Number.isFinite(result.cost)) {
470
+ return base;
471
+ }
472
+ if (!result.isAuthoritative && result.source === 'estimate-fallback') {
473
+ return base.costStatus ? base : { ...base, costStatus: 'unpriced' };
474
+ }
475
+ return {
476
+ cost: result.cost,
477
+ costStatus: 'priced',
478
+ ...(result.breakdown ? { costBreakdown: result.breakdown } : {})
479
+ };
480
+ }
437
481
  /**
438
482
  * Router cost passthrough, then optional @x12i/ai-tools catalog pricing when still unpriced.
439
483
  */
@@ -452,37 +496,114 @@ export async function resolveCostCompletionWithAiTools(routerResponse, tokens, o
452
496
  if (!hasNonZeroTokenUsage(tokens)) {
453
497
  return base;
454
498
  }
455
- const routing = pickInvokeRoutingMetadataSlice(routerResponse, options.mergedConfig);
456
- const cfg = options.mergedConfig != null && typeof options.mergedConfig === 'object'
457
- ? options.mergedConfig
458
- : {};
459
- const provider = routing.provider ?? cfg.provider;
460
- const modelUsed = routing.modelUsed ?? cfg.model;
461
- if (!provider || !modelUsed) {
462
- return base;
463
- }
464
499
  try {
465
- const result = await options.calculator.calculate({
466
- tokens: {
467
- prompt: tokens.prompt,
468
- completion: tokens.completion,
469
- total: tokens.total
470
- },
471
- provider,
472
- modelUsed
473
- });
474
- if (typeof result.cost === 'number' && Number.isFinite(result.cost)) {
475
- return {
476
- cost: result.cost,
477
- costStatus: 'priced',
478
- ...(result.breakdown ? { costBreakdown: result.breakdown } : {})
479
- };
480
- }
500
+ const record = buildGatewayPricingRecord(routerResponse, tokens, options.mergedConfig);
501
+ const result = await options.calculator.calculateFromRecord(record);
502
+ return mapAiCostResultToResolvedActivityCost(base, result);
481
503
  }
482
504
  catch {
483
- // Keep router/gateway unpriced fallback
505
+ const routing = pickInvokeRoutingMetadataSlice(routerResponse, options.mergedConfig);
506
+ const cfg = options.mergedConfig != null && typeof options.mergedConfig === 'object'
507
+ ? options.mergedConfig
508
+ : {};
509
+ const provider = routing.provider ?? cfg.provider;
510
+ const modelUsed = routing.modelUsed ?? cfg.model;
511
+ if (!provider || !modelUsed) {
512
+ return base;
513
+ }
514
+ try {
515
+ const result = await options.calculator.calculate({
516
+ tokens: {
517
+ prompt: tokens.prompt,
518
+ completion: tokens.completion,
519
+ total: tokens.total
520
+ },
521
+ provider,
522
+ usedModel: modelUsed
523
+ });
524
+ return mapAiCostResultToResolvedActivityCost(base, result);
525
+ }
526
+ catch {
527
+ return base;
528
+ }
484
529
  }
485
- return base;
530
+ }
531
+ function applyBillingToTraceAttempt(attempt, billing) {
532
+ if (billing.costStatus === 'priced' || billing.costStatus === 'unpriced') {
533
+ attempt.costStatus = billing.costStatus;
534
+ }
535
+ if (typeof billing.cost === 'number' && Number.isFinite(billing.cost)) {
536
+ attempt.costUsd = billing.cost;
537
+ }
538
+ if (billing.costBreakdown) {
539
+ attempt.costBreakdown = billing.costBreakdown;
540
+ }
541
+ }
542
+ function buildTraceAttemptPricingRecord(attempt, mergedConfig) {
543
+ const tokens = attempt.usage?.tokens ?? { prompt: 0, completion: 0, total: 0 };
544
+ return buildGatewayPricingRecord({
545
+ metadata: {
546
+ provider: attempt.routing.provider,
547
+ modelUsed: attempt.modelUsed,
548
+ region: attempt.routing.region,
549
+ tokens
550
+ }
551
+ }, tokens, mergedConfig);
552
+ }
553
+ /**
554
+ * Trace-mode summary: final token usage + resolved billing (after catalog pricing when applicable).
555
+ */
556
+ export function buildTraceUsageSummary(tokens, billing, maxTokensRequested) {
557
+ if (!hasNonZeroTokenUsage(tokens) && !billing.costStatus) {
558
+ return undefined;
559
+ }
560
+ const summary = { tokens };
561
+ if (maxTokensRequested !== undefined) {
562
+ summary.maxTokensRequested = maxTokensRequested;
563
+ }
564
+ if (billing.costStatus === 'priced' && typeof billing.cost === 'number') {
565
+ summary.costUsd = billing.cost;
566
+ summary.cost = billing.cost;
567
+ }
568
+ if (billing.costStatus) {
569
+ summary.costStatus = billing.costStatus;
570
+ }
571
+ if (billing.costBreakdown) {
572
+ summary.costBreakdown = billing.costBreakdown;
573
+ }
574
+ return summary;
575
+ }
576
+ /**
577
+ * Apply resolved billing to trace attempts: final successful attempt gets aggregate billing;
578
+ * other successful attempts without router cost get per-attempt catalog pricing when enabled.
579
+ */
580
+ export async function enrichTraceAttemptsWithBilling(attempts, finalBilling, options) {
581
+ if (!attempts.length)
582
+ return;
583
+ let lastOkIdx = -1;
584
+ for (let i = attempts.length - 1; i >= 0; i--) {
585
+ if (attempts[i].ok) {
586
+ lastOkIdx = i;
587
+ break;
588
+ }
589
+ }
590
+ if (lastOkIdx >= 0) {
591
+ applyBillingToTraceAttempt(attempts[lastOkIdx], finalBilling);
592
+ }
593
+ if (options?.calculateCost === false || !options?.calculator) {
594
+ return;
595
+ }
596
+ await Promise.all(attempts.map(async (attempt, idx) => {
597
+ if (!attempt.ok || idx === lastOkIdx)
598
+ return;
599
+ const tokens = attempt.usage?.tokens;
600
+ if (!tokens || !hasNonZeroTokenUsage(tokens))
601
+ return;
602
+ if (attempt.costStatus === 'priced' && typeof attempt.costUsd === 'number')
603
+ return;
604
+ const slice = await resolveCostCompletionWithAiTools(buildTraceAttemptPricingRecord(attempt, options.mergedConfig), tokens, options);
605
+ applyBillingToTraceAttempt(attempt, slice);
606
+ }));
486
607
  }
487
608
  /**
488
609
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
package/dist/gateway.js CHANGED
@@ -9,7 +9,7 @@ import { initializeGatewayComponents } from './gateway-config.js';
9
9
  import { buildMessages } from './message-builder.js';
10
10
  import { extractJsonFromFlexMd } from './flex-md-loader.js';
11
11
  import { enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
12
- import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, resolveCostCompletionWithAiTools, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
12
+ import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, resolveCostCompletionWithAiTools, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
13
13
  import { getAiToolsClient } from './ai-tools-client.js';
14
14
  import { autoRegisterProviders } from './gateway-provider-auto-register.js';
15
15
  import { setGatewayLastJobId, setGatewayRuntimeClients } from './runtime-objects.js';
@@ -567,6 +567,16 @@ export class AIGateway {
567
567
  const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
568
568
  const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
569
569
  const traceMergedRouterSnapshot = traceEnabled ? pickTraceMergedRouterConfig(mergedConfig) : undefined;
570
+ if (traceEnabled && traceAttempts) {
571
+ await enrichTraceAttemptsWithBilling(traceAttempts, costCompletion, {
572
+ mergedConfig,
573
+ calculator: aiTools?.calculator ?? null,
574
+ calculateCost: this.config.aiTools?.calculateCost
575
+ });
576
+ }
577
+ const traceUsageSummary = traceEnabled
578
+ ? buildTraceUsageSummary(tokens, costCompletion, routingMetadataSlice.maxTokensRequested)
579
+ : undefined;
570
580
  const enhancedResponse = {
571
581
  content: content,
572
582
  parsedContent: parsedContent,
@@ -597,6 +607,7 @@ export class AIGateway {
597
607
  retryCount: traceRetryCount,
598
608
  fallbackCount: traceFallbackCount,
599
609
  attempts: traceAttempts,
610
+ ...(traceUsageSummary !== undefined ? { usage: traceUsageSummary } : {}),
600
611
  ...(traceMergedRouterSnapshot !== undefined
601
612
  ? { mergedRouterConfig: traceMergedRouterSnapshot }
602
613
  : {})
package/dist/index.d.ts CHANGED
@@ -16,8 +16,8 @@ export * from '@x12i/ai-providers-router';
16
16
  export { AIGateway } from './gateway.js';
17
17
  export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
18
18
  export { autoRegisterProviders } from './gateway-provider-auto-register.js';
19
- export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions } from './types.js';
20
- export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, hasNonZeroTokenUsage } from './gateway-utils.js';
19
+ export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, GatewayTraceAttempt, GatewayTraceUsageSummary, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions } from './types.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, buildGatewayPricingRecord, mapAiCostResultToResolvedActivityCost, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, hasNonZeroTokenUsage } from './gateway-utils.js';
21
21
  export { getGatewayOperationalMode, isProdGatewayMode, resolveGatewayDefaultModel, parseModelProviderSpec, CODE_DEFAULT_MODEL } from './gateway-mode.js';
22
22
  export type { GatewayOperationalMode, GatewayDefaultModelSource, DefaultModelSubstitutionReason, ResolvedGatewayDefault } from './gateway-mode.js';
23
23
  export type { ActivityCostStatus, ResolvedActivityCost } from './gateway-utils.js';
@@ -29,7 +29,8 @@ export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory,
29
29
  export type { GatewayDualMemoryRoot } from './memory-path-resolution.js';
30
30
  export type { UsageTier } from './types.js';
31
31
  export { Activix } from '@x12i/activix';
32
- export type { ActivixRunContext, FindByRunContextCriteria, GetJobActivitiesInput, GetJobActivitiesResult } from '@x12i/activix';
32
+ export type { ActivixRunContext, ActivixAutoCostOptions, ActivixCostShape, FindByRunContextCriteria, GetJobActivitiesInput, GetJobActivitiesResult } from '@x12i/activix';
33
+ export { normalizeToActivixCostShape } from '@x12i/activix';
33
34
  export { ActivityManager, ensureGatewayRequestIdentity } from './activity-manager.js';
34
35
  export type { ActivityIdentity } from './types.js';
35
36
  export { activityIdentityToLogMeta, withActivityIdentity, gatewayLogDebug } from './gateway-log-meta.js';
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ export * from '@x12i/ai-providers-router';
17
17
  export { AIGateway } from './gateway.js';
18
18
  export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
19
19
  export { autoRegisterProviders } from './gateway-provider-auto-register.js';
20
- export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, hasNonZeroTokenUsage } from './gateway-utils.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, buildGatewayPricingRecord, mapAiCostResultToResolvedActivityCost, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, hasNonZeroTokenUsage } from './gateway-utils.js';
21
21
  export { getGatewayOperationalMode, isProdGatewayMode, resolveGatewayDefaultModel, parseModelProviderSpec, CODE_DEFAULT_MODEL } from './gateway-mode.js';
22
22
  export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
23
23
  export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
@@ -26,6 +26,7 @@ export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory,
26
26
  // (x-models was previously used for RPM/TPM tracking but is no longer integrated)
27
27
  // Re-export activity tracking primitives (Activix)
28
28
  export { Activix } from '@x12i/activix';
29
+ export { normalizeToActivixCostShape } from '@x12i/activix';
29
30
  export { ActivityManager, ensureGatewayRequestIdentity } from './activity-manager.js';
30
31
  export { activityIdentityToLogMeta, withActivityIdentity, gatewayLogDebug } from './gateway-log-meta.js';
31
32
  // Re-export logging (@x12i/logxer)
@@ -39,22 +40,5 @@ export { DEFAULT_RATE_LIMIT_MIN_INTERVAL_MS, DEFAULT_RATE_LIMIT_ENABLED } from '
39
40
  export { validateAIRequest, validateJSON, extractJSON, validateResponse, diagnoseRequest, diagnoseResponse, supportsJSONMode, createTestAIRequest, createValidationTestCases, runValidationTests, formatDiagnostic, assertValidAIRequest } from './troubleshooting-helper.js';
40
41
  // Export object types library
41
42
  export { OBJECT_TYPES_LIBRARY, getObjectType, getObjectTypesForAgent } from './object-types-library.js';
42
- // Re-export outputs library integration functions
43
+ // Object-types library stubs (optional @x12i/outputs-library integration; see object-types-library-integration.ts)
43
44
  export { initializeObjectTypesLibrary, getObjectTypesLibrary, resetObjectTypesLibrary } from './object-types-library-integration.js';
44
- // Re-export outputs library types and utilities for convenience
45
- // Note: Since we use dynamic imports for the outputs library, these types may not be available
46
- // at compile time if the package isn't installed. Users can import directly from
47
- // @x12i/outputs-library if they need these types or utilities.
48
- //
49
- // Recommended: Import types and utilities directly from @x12i/outputs-library:
50
- // import type { ClassificationOutput } from '@x12i/outputs-library/types';
51
- // import { ResponseParser } from '@x12i/outputs-library/parsers';
52
- // import type { ObjectTypesLibrary, FlexMdSupport } from '@x12i/outputs-library';
53
- //
54
- // The gateway integrates with the outputs library internally via dynamic imports,
55
- // so these re-exports are optional and mainly for convenience.
56
- //
57
- // For outputs-library v3.3.1+ with flex-md support:
58
- // - ObjectTypesLibrary class with flex-md methods (getFlexMdTemplate, getFlexMdFormatSpec, etc.)
59
- // - FlexMdSupport type for object type definitions
60
- // - All flex-md methods are available on the library instance returned by getObjectTypesLibrary()
package/dist/types.d.ts CHANGED
@@ -73,6 +73,17 @@ export type GatewayTraceAttempt = {
73
73
  };
74
74
  modelUsed?: string;
75
75
  costUsd?: number;
76
+ /** Billing state for this attempt (trace mode; mirrors top-level {@link EnhancedLLMResponse.metadata.costStatus}). */
77
+ costStatus?: 'priced' | 'unpriced';
78
+ costBreakdown?: {
79
+ promptCostUsd: number;
80
+ completionCostUsd: number;
81
+ cachingCostUsd?: number;
82
+ reasoningCostUsd?: number;
83
+ audioCostUsd?: number;
84
+ imageCostUsd?: number;
85
+ requestFlatCostUsd?: number;
86
+ };
76
87
  ok: boolean;
77
88
  error?: {
78
89
  name: string;
@@ -88,6 +99,22 @@ export type GatewayTraceAttempt = {
88
99
  * Allowlisted merged router/generation config returned in {@link EnhancedLLMResponse.metadata}
89
100
  * when `diagnostics.mode === 'trace'`. Omits arbitrary extras and secrets.
90
101
  */
102
+ /**
103
+ * Consolidated usage + billing summary on {@link EnhancedLLMResponse.metadata} when
104
+ * `diagnostics.mode === 'trace'` (single object for orchestrators / Run Analysis).
105
+ */
106
+ export type GatewayTraceUsageSummary = {
107
+ tokens: {
108
+ prompt: number;
109
+ completion: number;
110
+ total: number;
111
+ };
112
+ maxTokensRequested?: number;
113
+ costUsd?: number;
114
+ cost?: number;
115
+ costStatus?: 'priced' | 'unpriced';
116
+ costBreakdown?: GatewayTraceAttempt['costBreakdown'];
117
+ };
91
118
  export type GatewayTraceMergedConfig = Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP' | 'frequencyPenalty' | 'presencePenalty' | 'stop'>>;
92
119
  /**
93
120
  * Normalized observability payload attached to thrown errors from {@link AIGateway.invoke}
@@ -348,13 +375,15 @@ export interface GatewayConfig extends Omit<RouterConfig, 'defaultEngine' | 'log
348
375
  mode?: 'dev' | 'debug' | 'prod';
349
376
  /**
350
377
  * @x12i/ai-tools integration: catalog model resolution (request) and cost calculation (response).
378
+ * Pricing catalogs load from open-assets JSON (remote with bundled fallback).
351
379
  */
352
380
  aiTools?: {
353
381
  /** @default true */
354
382
  enabled?: boolean;
355
- /** Inject Catalox; otherwise `createCataloxFromEnv()` from `@x12i/catalox/firebase`. */
356
- catalox?: import('@x12i/catalox').Catalox;
383
+ /** In-memory catalog cache TTL (ms). Default in ai-tools is 24h. */
357
384
  cacheTtlMs?: number;
385
+ /** Use bundled catalog JSON only (offline / tests). */
386
+ bundledOnly?: boolean;
358
387
  /** @default true */
359
388
  resolveModels?: boolean;
360
389
  /** @default true */
@@ -1009,6 +1038,11 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
1009
1038
  * Ordered, authoritative attempts across retries and fallbacks (trace mode).
1010
1039
  */
1011
1040
  attempts?: GatewayTraceAttempt[];
1041
+ /**
1042
+ * Final usage + billing for the invocation (trace mode). Mirrors successful-attempt
1043
+ * tokens and {@link costUsd} / {@link costStatus} after router passthrough + catalog pricing.
1044
+ */
1045
+ usage?: GatewayTraceUsageSummary;
1012
1046
  /**
1013
1047
  * Merged gateway/router generation config actually used for the invocation (after
1014
1048
  * {@link mergeConfig}: modelConfig / request.config / defaults / flex-md maxTokens).