@x12i/ai-gateway 9.3.5 → 9.5.2

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
@@ -193,6 +198,9 @@ export declare class ActivityManager {
193
198
  * @returns Activix instance or undefined if not enabled
194
199
  */
195
200
  getTracker(): Activix | undefined;
201
+ /** Await Activix init (no-op when tracking is disabled). */
202
+ getReadyTracker(): Promise<Activix | undefined>;
203
+ private logActivixBackendReady;
196
204
  /**
197
205
  * Get status of activity tracker
198
206
  */
@@ -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 } : {})
202
193
  };
194
+ return normalizeToActivixCostShape(candidate, 'gateway.outer.cost') ?? undefined;
203
195
  }
204
- /** Routing / generation facts for Activix `outer.metadata` on completion (includes billing mirror). */
205
- function pickActivixCompletionRoutingMetadata(response, billing) {
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)
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;
212
+ }
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,41 +369,31 @@ export class ActivityManager {
385
369
  exponentialBackoff: false
386
370
  }
387
371
  }
388
- });
389
- this.initPromise = this.activix
390
- .init()
391
- .then(() => {
392
- const ax = this.activix;
393
- if (!ax) {
394
- return;
395
- }
396
- const backend = ax.storageBackend;
397
- const mongoDb = backend === 'database' ? resolveActivixLogsDatabaseName() : undefined;
398
- const mongoUriConfigured = Boolean(resolveActivixMongoUriFromEnv());
399
- this.logger.info('Activity tracking persistence backend ready', {
400
- storageBackend: backend,
401
- mongoDatabase: mongoDb,
402
- mongoUriConfigured,
403
- mainCollection: collectionName,
404
- badRequestsCollection: badRequestsCollectionName,
405
- skillExecutionsCollection: this.skillExecutionsCollectionName,
406
- ...(backend === 'local'
407
- ? {
408
- note: 'Activix is using local playground storage, not MongoDB. The ai-actions collection will not appear in Mongo until URI is set (MONGO_URI or MONGO_LOGS_URI), Activix can ping the database, and at least one activity is written.'
409
- }
410
- : {
411
- note: 'MongoDB stores one document per activity; the ai-actions collection is created on first insert (empty collections may be hidden in some tools until then).'
412
- })
413
- });
414
- })
415
- .catch((error) => {
416
- // Init threw — disable tracker so requests are not blocked.
417
- this.logger.warn('Activity tracking enabled but Activix init failed. Activity records will not be persisted.', {
418
- error: error instanceof Error ? error.message : String(error),
419
- hint: 'Set MONGO_URI or MONGO_LOGS_URI and a database name (MONGO_LOGS_DB, MONGO_DB, MONGO_AI_LOGS_DB, or ACTIVIX_DB_NAME). See README: Activity tracking / persistence troubleshooting.'
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
+ if (config.customTracker) {
380
+ this.activix = config.customTracker;
381
+ this.initPromise = Promise.resolve().then(() => this.logActivixBackendReady(collectionName, badRequestsCollectionName));
382
+ }
383
+ else {
384
+ this.initPromise = Activix.create(activixOptions)
385
+ .then((ax) => {
386
+ this.activix = ax;
387
+ this.logActivixBackendReady(collectionName, badRequestsCollectionName);
388
+ })
389
+ .catch((error) => {
390
+ this.logger.warn('Activity tracking enabled but Activix init failed. Activity records will not be persisted.', {
391
+ error: error instanceof Error ? error.message : String(error),
392
+ hint: 'Set MONGO_URI or MONGO_LOGS_URI and a database name (MONGO_LOGS_DB, MONGO_DB, MONGO_AI_LOGS_DB, or ACTIVIX_DB_NAME). See README: Activity tracking / persistence troubleshooting.'
393
+ });
394
+ this.activix = undefined;
420
395
  });
421
- this.activix = undefined;
422
- });
396
+ }
423
397
  this.logger.debug('Activity tracking enabled with Activix', {
424
398
  collection: collectionName,
425
399
  badRequestsCollection: badRequestsCollectionName,
@@ -939,8 +913,9 @@ export class ActivityManager {
939
913
  costStatus: details.costStatus,
940
914
  costBreakdown: details.costBreakdown
941
915
  };
942
- const outerMetadata = pickActivixCompletionRoutingMetadata(details.response, billingSlice);
916
+ const outerMetadata = pickActivixCompletionRoutingMetadata(details.response);
943
917
  const outerCost = buildActivixOuterCost(outerMetadata, billingSlice, details.response);
918
+ const recordMetadata = buildActivixRecordMetadata(details.response, billingSlice);
944
919
  await this.activix.completeRecord(activity.activityId, {
945
920
  cost: details.cost,
946
921
  ...(typeof details.cost === 'number' && Number.isFinite(details.cost)
@@ -948,13 +923,12 @@ export class ActivityManager {
948
923
  : {}),
949
924
  ...(details.costStatus ? { costStatus: details.costStatus } : {}),
950
925
  response: details.response,
926
+ ...(Object.keys(recordMetadata).length > 0 ? { metadata: recordMetadata } : {}),
951
927
  outer: {
952
928
  output: details.response,
953
929
  metadata: outerMetadata,
954
930
  ...(outerCost ? { cost: outerCost } : {})
955
- },
956
- endTime: details.endTime,
957
- duration: details.duration
931
+ }
958
932
  }, { collection });
959
933
  this.logger.debug('Activix.completeRecord completed', {
960
934
  aiRequestId: activity.aiRequestId,
@@ -1224,6 +1198,36 @@ export class ActivityManager {
1224
1198
  getTracker() {
1225
1199
  return this.activix;
1226
1200
  }
1201
+ /** Await Activix init (no-op when tracking is disabled). */
1202
+ async getReadyTracker() {
1203
+ if (this.initPromise) {
1204
+ await this.initPromise;
1205
+ }
1206
+ return this.activix;
1207
+ }
1208
+ logActivixBackendReady(collectionName, badRequestsCollectionName) {
1209
+ const ax = this.activix;
1210
+ if (!ax)
1211
+ return;
1212
+ const backend = ax.storageBackend;
1213
+ const mongoDb = backend === 'database' ? resolveActivixLogsDatabaseName() : undefined;
1214
+ const mongoUriConfigured = Boolean(resolveActivixMongoUriFromEnv());
1215
+ this.logger.info('Activity tracking persistence backend ready', {
1216
+ storageBackend: backend,
1217
+ mongoDatabase: mongoDb,
1218
+ mongoUriConfigured,
1219
+ mainCollection: collectionName,
1220
+ badRequestsCollection: badRequestsCollectionName,
1221
+ skillExecutionsCollection: this.skillExecutionsCollectionName,
1222
+ ...(backend === 'local'
1223
+ ? {
1224
+ note: 'Activix is using local playground storage, not MongoDB. The ai-actions collection will not appear in Mongo until URI is set (MONGO_URI or MONGO_LOGS_URI), Activix can ping the database, and at least one activity is written.'
1225
+ }
1226
+ : {
1227
+ note: 'MongoDB stores one document per activity; the ai-actions collection is created on first insert (empty collections may be hidden in some tools until then).'
1228
+ })
1229
+ });
1230
+ }
1227
1231
  /**
1228
1232
  * Get status of activity tracker
1229
1233
  */
@@ -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
@@ -6,6 +6,7 @@ import type { GatewayConfig } from './types.js';
6
6
  import type { Logxer } from '@x12i/logxer';
7
7
  import { LLMProviderRouter } from '@x12i/ai-providers-router';
8
8
  import { ActivityManager } from './activity-manager.js';
9
+ import { OptimixerManager } from './optimixer-manager.js';
9
10
  import { UsageTracker } from './usage-tracker.js';
10
11
  import type { MessageBuilderConfig } from './message-builder.js';
11
12
  import type { TemplateRenderOptions } from '@x12i/rendrix';
@@ -16,6 +17,7 @@ export interface GatewayConfigContext {
16
17
  logger: Logxer;
17
18
  router: LLMProviderRouter;
18
19
  activityManager: ActivityManager;
20
+ optimixerManager: OptimixerManager;
19
21
  usageTracker: UsageTracker;
20
22
  messageBuilderConfig: MessageBuilderConfig;
21
23
  }
@@ -45,6 +47,7 @@ export declare function initializeGatewayComponents(config: GatewayConfig): {
45
47
  logger: Logxer;
46
48
  router: LLMProviderRouter;
47
49
  activityManager: ActivityManager;
50
+ optimixerManager: OptimixerManager;
48
51
  usageTracker: UsageTracker;
49
52
  messageBuilderConfig: MessageBuilderConfig;
50
53
  defaultModelConfig: Record<string, unknown>;
@@ -48,6 +48,7 @@ function getDefaultsDir() {
48
48
  import { LLMProviderRouter } from '@x12i/ai-providers-router';
49
49
  import { createGatewayLogger } from './logger-factory.js';
50
50
  import { ActivityManager } from './activity-manager.js';
51
+ import { OptimixerManager } from './optimixer-manager.js';
51
52
  import { UsageTracker } from './usage-tracker.js';
52
53
  import { mergeTemplateRenderOptions } from './template-render-merge.js';
53
54
  import { GatewayRateLimiter } from './gateway-rate-limiter.js';
@@ -265,7 +266,23 @@ export function initializeGatewayComponents(config) {
265
266
  const activityManager = new ActivityManager({
266
267
  enableActivityTracking: config.enableActivityTracking ?? true,
267
268
  customTracker: config.activityTracker,
268
- logger
269
+ logger,
270
+ ...(config.activityTracker
271
+ ? {}
272
+ : {
273
+ autoCost: config.aiTools?.enabled === false || config.aiTools?.calculateCost === false
274
+ ? false
275
+ : {
276
+ enabled: true,
277
+ overwriteOuterCost: false,
278
+ ...(config.aiTools?.bundledOnly ? { bundledOnly: true } : {})
279
+ }
280
+ })
281
+ });
282
+ const optimixerManager = new OptimixerManager({
283
+ optimixer: config.optimixer,
284
+ logger,
285
+ getActivix: () => activityManager.getReadyTracker()
269
286
  });
270
287
  const templateRendering = mergeTemplateRenderOptions(defaultTemplateRendering, config.templateRendering);
271
288
  const instructionsBlockOverrides = {
@@ -282,6 +299,7 @@ export function initializeGatewayComponents(config) {
282
299
  logger,
283
300
  router,
284
301
  activityManager,
302
+ optimixerManager,
285
303
  usageTracker,
286
304
  messageBuilderConfig,
287
305
  defaultModelConfig
@@ -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
  */
@@ -17,6 +17,12 @@ export type MergeConfigOptions = {
17
17
  defaultModelConfig?: Record<string, unknown>;
18
18
  catalog?: AiModelsCatalogClient | null;
19
19
  };
20
+ /**
21
+ * True when any caller-controlled config source set `maxTokens` (Optimixer should not override).
22
+ */
23
+ export declare function isMaxTokensExplicitlySet(request: ChatRequest & {
24
+ useInternalDefaults?: 'skill' | 'audit';
25
+ }, config: GatewayConfig): boolean;
20
26
  /**
21
27
  * Merges config with defaults
22
28
  * Supports using internal system action defaults (internalSkill or skillAudit) when useInternalDefaults is set
@@ -91,6 +97,13 @@ export type ResolveCostCompletionOptions = {
91
97
  calculator?: CostCalculator | null;
92
98
  calculateCost?: boolean;
93
99
  };
100
+ /** Record shape for {@link CostCalculator.calculateFromRecord} (router + merged config + usage). */
101
+ export declare function buildGatewayPricingRecord(routerResponse: unknown, tokens: {
102
+ prompt: number;
103
+ completion: number;
104
+ total: number;
105
+ }, mergedConfig?: unknown): Record<string, unknown>;
106
+ export declare function mapAiCostResultToResolvedActivityCost(base: ResolvedActivityCost, result: AiCostResult): ResolvedActivityCost;
94
107
  /**
95
108
  * Router cost passthrough, then optional @x12i/ai-tools catalog pricing when still unpriced.
96
109
  */
@@ -99,6 +112,19 @@ export declare function resolveCostCompletionWithAiTools(routerResponse: unknown
99
112
  completion: number;
100
113
  total: number;
101
114
  }, options?: ResolveCostCompletionOptions): Promise<ResolvedActivityCost>;
115
+ /**
116
+ * Trace-mode summary: final token usage + resolved billing (after catalog pricing when applicable).
117
+ */
118
+ export declare function buildTraceUsageSummary(tokens: {
119
+ prompt: number;
120
+ completion: number;
121
+ total: number;
122
+ }, billing: ResolvedActivityCost, maxTokensRequested?: number): GatewayTraceUsageSummary | undefined;
123
+ /**
124
+ * Apply resolved billing to trace attempts: final successful attempt gets aggregate billing;
125
+ * other successful attempts without router cost get per-attempt catalog pricing when enabled.
126
+ */
127
+ export declare function enrichTraceAttemptsWithBilling(attempts: GatewayTraceAttempt[], finalBilling: ResolvedActivityCost, options?: ResolveCostCompletionOptions): Promise<void>;
102
128
  /**
103
129
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
104
130
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
@@ -145,4 +171,10 @@ export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
145
171
  * Non-serializable values become a small marker object instead of throwing.
146
172
  */
147
173
  export declare function capActivityFullResponsePayload(payload: unknown, maxChars?: number): unknown;
174
+ export declare function resolveFinishReasonFromRouterResponse(response: unknown): string | undefined;
175
+ export declare function buildOptimixerActualUsage(tokens: {
176
+ prompt: number;
177
+ completion: number;
178
+ total: number;
179
+ }, response: unknown, latencyMs: number): import('@x12i/optimixer').AiMaxTokensActualUsage;
148
180
  export {};