@x12i/ai-gateway 9.1.6 → 9.3.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.
Files changed (78) hide show
  1. package/README.md +45 -0
  2. package/dist/activity-manager.d.ts +1 -0
  3. package/dist/activity-manager.js +7 -0
  4. package/dist/ai-tools-client.d.ts +20 -0
  5. package/dist/ai-tools-client.js +91 -0
  6. package/dist/flex-md-loader.d.ts +5 -0
  7. package/dist/flex-md-loader.js +16 -0
  8. package/dist/gateway-config.d.ts +2 -0
  9. package/dist/gateway-config.js +2 -1
  10. package/dist/gateway-mode.d.ts +40 -0
  11. package/dist/gateway-mode.js +75 -0
  12. package/dist/gateway-utils.d.ts +57 -1
  13. package/dist/gateway-utils.js +181 -12
  14. package/dist/gateway.d.ts +3 -0
  15. package/dist/gateway.js +47 -15
  16. package/dist/index.d.ts +6 -1
  17. package/dist/index.js +3 -1
  18. package/dist/output-contract-normalizer.d.ts +21 -0
  19. package/dist/output-contract-normalizer.js +121 -0
  20. package/dist/types.d.ts +35 -0
  21. package/dist-cjs/activity-manager.cjs +21 -19
  22. package/dist-cjs/activity-manager.d.ts +1 -0
  23. package/dist-cjs/ai-tools-client.cjs +91 -0
  24. package/dist-cjs/ai-tools-client.d.ts +20 -0
  25. package/dist-cjs/config/activity-tracking-config.cjs +1 -4
  26. package/dist-cjs/content-normalizer/content-normalizer.cjs +3 -8
  27. package/dist-cjs/content-normalizer/index.cjs +1 -7
  28. package/dist-cjs/content-normalizer/types.cjs +1 -2
  29. package/dist-cjs/flex-md-loader.cjs +35 -65
  30. package/dist-cjs/flex-md-loader.d.ts +5 -0
  31. package/dist-cjs/gateway-config.cjs +25 -63
  32. package/dist-cjs/gateway-config.d.ts +2 -0
  33. package/dist-cjs/gateway-conversion.cjs +10 -48
  34. package/dist-cjs/gateway-instructions.cjs +5 -10
  35. package/dist-cjs/gateway-log-meta.cjs +9 -14
  36. package/dist-cjs/gateway-memory.cjs +2 -6
  37. package/dist-cjs/gateway-messages.cjs +3 -6
  38. package/dist-cjs/gateway-meta.cjs +1 -4
  39. package/dist-cjs/gateway-mode.cjs +75 -0
  40. package/dist-cjs/gateway-mode.d.ts +40 -0
  41. package/dist-cjs/gateway-provider-auto-register.cjs +2 -38
  42. package/dist-cjs/gateway-provider.cjs +10 -22
  43. package/dist-cjs/gateway-rate-limiter-constants.cjs +2 -5
  44. package/dist-cjs/gateway-rate-limiter.cjs +5 -9
  45. package/dist-cjs/gateway-retry.cjs +6 -14
  46. package/dist-cjs/gateway-utils.cjs +201 -83
  47. package/dist-cjs/gateway-utils.d.ts +57 -1
  48. package/dist-cjs/gateway-validation.cjs +2 -6
  49. package/dist-cjs/gateway.cjs +100 -72
  50. package/dist-cjs/gateway.d.ts +3 -0
  51. package/dist-cjs/index.cjs +22 -91
  52. package/dist-cjs/index.d.ts +6 -1
  53. package/dist-cjs/instruction-errors.cjs +2 -7
  54. package/dist-cjs/instruction-optimizer.cjs +4 -10
  55. package/dist-cjs/instructions-parser.cjs +5 -10
  56. package/dist-cjs/logger-factory.cjs +3 -6
  57. package/dist-cjs/memory-path-resolution.cjs +8 -18
  58. package/dist-cjs/message-builder.cjs +11 -47
  59. package/dist-cjs/object-types-library-integration.cjs +3 -8
  60. package/dist-cjs/object-types-library.cjs +5 -10
  61. package/dist-cjs/output-auditor.cjs +1 -4
  62. package/dist-cjs/output-contract-normalizer.cjs +121 -0
  63. package/dist-cjs/output-contract-normalizer.d.ts +21 -0
  64. package/dist-cjs/request-report-generator.cjs +1 -4
  65. package/dist-cjs/response-analyzer/format-type-detector.cjs +1 -5
  66. package/dist-cjs/response-analyzer/index.cjs +3 -9
  67. package/dist-cjs/response-analyzer/object-type-detector.cjs +1 -5
  68. package/dist-cjs/response-analyzer/response-analyzer.cjs +6 -10
  69. package/dist-cjs/response-analyzer/types.cjs +1 -2
  70. package/dist-cjs/response-fallback-fixer.cjs +1 -4
  71. package/dist-cjs/runtime-objects.cjs +7 -13
  72. package/dist-cjs/template-parser.cjs +5 -42
  73. package/dist-cjs/template-render-merge.cjs +2 -6
  74. package/dist-cjs/troubleshooting-helper.cjs +13 -28
  75. package/dist-cjs/types.cjs +1 -2
  76. package/dist-cjs/types.d.ts +35 -0
  77. package/dist-cjs/usage-tracker.cjs +3 -7
  78. package/package.json +11 -5
@@ -3,8 +3,11 @@
3
3
  * Handles utility functions
4
4
  */
5
5
  import * as crypto from 'crypto';
6
+ import { ModelResolutionError } from '@x12i/ai-tools';
6
7
  import { getPreParsedInstructions } from './gateway-instructions.js';
7
8
  import { getModelMaxTokensFromFlexMd } from './flex-md-loader.js';
9
+ import { applyModelResolution } from './ai-tools-client.js';
10
+ import { getGatewayOperationalMode, isProdGatewayMode, resolveGatewayDefaultModel, warnDefaultModelSubstitution } from './gateway-mode.js';
8
11
  /**
9
12
  * Generates MD5 hash of a string
10
13
  */
@@ -29,11 +32,34 @@ export async function ensureTaskTypeId(request, logger) {
29
32
  });
30
33
  return taskTypeId;
31
34
  }
35
+ function applyGatewayDefaultToMerged(merged, defaults, config) {
36
+ merged.model = defaults.model;
37
+ if (defaults.provider) {
38
+ merged.provider = defaults.provider;
39
+ }
40
+ else if (!merged.provider) {
41
+ merged.provider = config.defaultEngine;
42
+ }
43
+ }
44
+ async function substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, reason, original) {
45
+ const operationalMode = getGatewayOperationalMode(config);
46
+ const defaults = resolveGatewayDefaultModel(mergeOptions?.defaultModelConfig, config.defaultEngine);
47
+ warnDefaultModelSubstitution(logger, request.identity, {
48
+ reason,
49
+ mode: operationalMode,
50
+ defaultSource: defaults.source,
51
+ defaultProvider: defaults.provider ?? merged.provider,
52
+ defaultModel: defaults.model,
53
+ originalProvider: original?.provider ?? merged.provider,
54
+ originalModel: original?.model
55
+ });
56
+ applyGatewayDefaultToMerged(merged, defaults, config);
57
+ }
32
58
  /**
33
59
  * Merges config with defaults
34
60
  * Supports using internal system action defaults (internalSkill or skillAudit) when useInternalDefaults is set
35
61
  */
36
- export async function mergeConfig(request, config, logger) {
62
+ export async function mergeConfig(request, config, logger, mergeOptions) {
37
63
  const useInternalDefaults = request.useInternalDefaults;
38
64
  const internalDefaults = useInternalDefaults
39
65
  ? (useInternalDefaults === 'skill'
@@ -52,8 +78,8 @@ export async function mergeConfig(request, config, logger) {
52
78
  useInternalDefaults,
53
79
  hasInternalDefaults: !!internalDefaults
54
80
  });
55
- // Default model to "gpt-5-nano" if nothing is provided (most permissive - always works)
56
- const defaultModel = 'gpt-5-nano';
81
+ const operationalMode = getGatewayOperationalMode(config);
82
+ const resolveModels = config.aiTools?.resolveModels !== false;
57
83
  // Priority: modelConfig > request.config > internalSystemActions[useInternalDefaults] > gateway defaults
58
84
  // First, merge modelConfig into a config-like object if present
59
85
  const modelConfigAsConfig = request.modelConfig ? {
@@ -87,18 +113,67 @@ export async function mergeConfig(request, config, logger) {
87
113
  ...request.config,
88
114
  // ModelConfig overrides (highest priority) - merge only defined values
89
115
  ...(modelConfigAsConfig ? Object.fromEntries(Object.entries(modelConfigAsConfig).filter(([_, value]) => value !== undefined)) : {}),
90
- // Ensure model is set: modelConfig > request.config > internalDefaults > default
91
- model: modelConfigAsConfig?.model || request.config?.model || internalDefaults?.model || defaultModel,
116
+ // Model resolved below (catalog, default chain, or explicit pass-through)
117
+ model: modelConfigAsConfig?.model || request.config?.model || internalDefaults?.model,
92
118
  // Ensure provider is set: modelConfig > request.config > internalDefaults > gateway default
93
- // Provider is required for router to know which provider to use
94
119
  provider: modelConfigAsConfig?.provider || request.config?.provider || internalDefaults?.engine || config.defaultEngine
95
120
  };
96
- // Log if using default model
97
- if (!request.config?.model && !internalDefaults?.model) {
98
- logger.info('Using default model: gpt-5-nano (no model provided in request)', {
99
- jobId: request.identity.jobId,
100
- note: 'Default model ensures requests always work regardless of configuration'
101
- });
121
+ const explicitModel = merged.model;
122
+ const originalProvider = merged.provider;
123
+ const originalModel = explicitModel;
124
+ if (!explicitModel) {
125
+ await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'no_model_provided');
126
+ }
127
+ else if (resolveModels && mergeOptions?.catalog) {
128
+ try {
129
+ const resolution = await mergeOptions.catalog.resolveModel({
130
+ provider: merged.provider,
131
+ model: explicitModel
132
+ });
133
+ if (resolution.found) {
134
+ applyModelResolution(merged, resolution, config.defaultEngine);
135
+ request._modelResolution = {
136
+ modelId: resolution.modelId,
137
+ routedViaOpenRouter: resolution.routedViaOpenRouter,
138
+ confidence: resolution.confidence,
139
+ resolvedVia: resolution.resolvedVia,
140
+ originalProvider,
141
+ originalModel
142
+ };
143
+ logger.verbose('Catalog resolved model name', {
144
+ jobId: request.identity.jobId,
145
+ originalModel,
146
+ resolvedModelId: resolution.modelId,
147
+ provider: merged.provider,
148
+ model: merged.model,
149
+ confidence: resolution.confidence,
150
+ resolvedVia: resolution.resolvedVia
151
+ });
152
+ }
153
+ else if (isProdGatewayMode(operationalMode)) {
154
+ await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'model_resolution_failed', { provider: originalProvider, model: originalModel });
155
+ }
156
+ else {
157
+ throw new ModelResolutionError({ provider: merged.provider, model: explicitModel }, resolution);
158
+ }
159
+ }
160
+ catch (error) {
161
+ if (error instanceof ModelResolutionError) {
162
+ throw error;
163
+ }
164
+ if (isProdGatewayMode(operationalMode)) {
165
+ await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'ai_tools_unavailable', { provider: originalProvider, model: originalModel });
166
+ }
167
+ else {
168
+ throw error;
169
+ }
170
+ }
171
+ }
172
+ else if (resolveModels && !mergeOptions?.catalog && isProdGatewayMode(operationalMode)) {
173
+ await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'ai_tools_unavailable', { provider: originalProvider, model: originalModel });
174
+ }
175
+ if (!merged.model) {
176
+ await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'no_model_provided');
102
177
  }
103
178
  // Auto-get maxTokens from flex-md if not explicitly set in ANY config source
104
179
  // Check all possible sources: request.config, internalDefaults, gateway config
@@ -315,6 +390,100 @@ export function extractCostUsdFromRouterResponse(routerResponse) {
315
390
  }
316
391
  return undefined;
317
392
  }
393
+ export function hasNonZeroTokenUsage(tokens) {
394
+ return !!(tokens.prompt || tokens.completion || tokens.total);
395
+ }
396
+ function pickRouterCostStatus(routerResponse) {
397
+ if (routerResponse == null || typeof routerResponse !== 'object')
398
+ return undefined;
399
+ const r = routerResponse;
400
+ const meta = r.metadata != null && typeof r.metadata === 'object'
401
+ ? r.metadata
402
+ : undefined;
403
+ const status = meta?.costStatus ?? r.costStatus;
404
+ return status === 'priced' || status === 'unpriced' ? status : undefined;
405
+ }
406
+ /**
407
+ * Gateway fallback when the router does not set `metadata.costStatus`.
408
+ * Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
409
+ */
410
+ export function resolveActivityCostCompletion(tokens, costUsd) {
411
+ if (typeof costUsd === 'number' && Number.isFinite(costUsd)) {
412
+ return { cost: costUsd, costStatus: 'priced' };
413
+ }
414
+ if (hasNonZeroTokenUsage(tokens)) {
415
+ return { costStatus: 'unpriced' };
416
+ }
417
+ return {};
418
+ }
419
+ /**
420
+ * Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
421
+ * otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
422
+ */
423
+ export function resolveCostCompletionForActivity(routerResponse, tokens) {
424
+ const routerStatus = pickRouterCostStatus(routerResponse);
425
+ const costUsd = extractCostUsdFromRouterResponse(routerResponse);
426
+ if (routerStatus === 'priced') {
427
+ return {
428
+ ...(typeof costUsd === 'number' && Number.isFinite(costUsd) ? { cost: costUsd } : {}),
429
+ costStatus: 'priced'
430
+ };
431
+ }
432
+ if (routerStatus === 'unpriced') {
433
+ return { costStatus: 'unpriced' };
434
+ }
435
+ return resolveActivityCostCompletion(tokens, costUsd);
436
+ }
437
+ /**
438
+ * Router cost passthrough, then optional @x12i/ai-tools catalog pricing when still unpriced.
439
+ */
440
+ export async function resolveCostCompletionWithAiTools(routerResponse, tokens, options) {
441
+ const routerStatus = pickRouterCostStatus(routerResponse);
442
+ const base = resolveCostCompletionForActivity(routerResponse, tokens);
443
+ if (base.costStatus === 'priced') {
444
+ return base;
445
+ }
446
+ if (routerStatus === 'unpriced') {
447
+ return base;
448
+ }
449
+ if (options?.calculateCost === false || !options?.calculator) {
450
+ return base;
451
+ }
452
+ if (!hasNonZeroTokenUsage(tokens)) {
453
+ return base;
454
+ }
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
+ 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
+ }
481
+ }
482
+ catch {
483
+ // Keep router/gateway unpriced fallback
484
+ }
485
+ return base;
486
+ }
318
487
  /**
319
488
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
320
489
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
package/dist/gateway.d.ts CHANGED
@@ -16,7 +16,9 @@ export declare class AIGateway {
16
16
  private logger;
17
17
  private activityManager?;
18
18
  private messageBuilderConfig?;
19
+ private defaultModelConfig;
19
20
  private _autoRegisterDone;
21
+ private _aiToolsClient;
20
22
  constructor(config?: GatewayConfig, activityManager?: ActivityManager);
21
23
  /**
22
24
  * Invoke chat request (without structured output requirements)
@@ -36,4 +38,5 @@ export declare class AIGateway {
36
38
  getLogger(): Logxer;
37
39
  getActivityManager(): ActivityManager | undefined;
38
40
  setActivityManager(activityManager: ActivityManager): void;
41
+ private getAiTools;
39
42
  }
package/dist/gateway.js CHANGED
@@ -8,7 +8,9 @@ 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 { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
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';
13
+ import { getAiToolsClient } from './ai-tools-client.js';
12
14
  import { autoRegisterProviders } from './gateway-provider-auto-register.js';
13
15
  import { setGatewayLastJobId, setGatewayRuntimeClients } from './runtime-objects.js';
14
16
  import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
@@ -44,7 +46,9 @@ export class AIGateway {
44
46
  logger;
45
47
  activityManager;
46
48
  messageBuilderConfig;
49
+ defaultModelConfig = {};
47
50
  _autoRegisterDone = false;
51
+ _aiToolsClient = null;
48
52
  constructor(config = {}, activityManager) {
49
53
  this.config = config;
50
54
  this.activityManager = activityManager;
@@ -53,6 +57,7 @@ export class AIGateway {
53
57
  this.router = components.router;
54
58
  this.activityManager = components.activityManager;
55
59
  this.messageBuilderConfig = components.messageBuilderConfig;
60
+ this.defaultModelConfig = components.defaultModelConfig ?? {};
56
61
  setGatewayRuntimeClients({
57
62
  activix: this.activityManager?.getTracker(),
58
63
  logger: this.logger
@@ -76,7 +81,11 @@ export class AIGateway {
76
81
  // Simple message construction
77
82
  const messages = this.buildSimpleMessages(request);
78
83
  // Merge config (modelConfig > request.config > gateway defaults)
79
- const mergedConfig = await mergeConfig(request, this.config, this.logger);
84
+ const aiTools = await this.getAiTools();
85
+ const mergedConfig = await mergeConfig(request, this.config, this.logger, {
86
+ defaultModelConfig: this.defaultModelConfig,
87
+ catalog: aiTools?.catalog ?? null
88
+ });
80
89
  // Activix start snapshot must match what the router receives (modelConfig-only callers omit request.config.model).
81
90
  request._mergedRouterConfig = mergedConfig;
82
91
  // Lazy auto-register providers from env (OPENAI_API_KEY, etc.) so consumers don't have to call init
@@ -108,8 +117,13 @@ export class AIGateway {
108
117
  },
109
118
  mode: 'sync'
110
119
  });
111
- const costUsdChat = extractCostUsdFromRouterResponse(response);
112
120
  const metaChat = response?.metadata || {};
121
+ const tokensChat = extractTokenUsageFromRouterResponse(response);
122
+ const costCompletionChat = await resolveCostCompletionWithAiTools(response, tokensChat, {
123
+ mergedConfig,
124
+ calculator: aiTools?.calculator ?? null,
125
+ calculateCost: this.config.aiTools?.calculateCost
126
+ });
113
127
  // Create enhanced response
114
128
  const enhancedResponse = {
115
129
  content: response.content || '',
@@ -117,22 +131,25 @@ export class AIGateway {
117
131
  aiRequestId: request.aiRequestId,
118
132
  identity: request.identity,
119
133
  latencyMs: Date.now() - startTime,
120
- tokens: extractTokenUsageFromRouterResponse(response),
134
+ tokens: tokensChat,
121
135
  taskTypeId,
122
136
  agentType: 'chat',
123
- ...(typeof costUsdChat === 'number'
137
+ ...(costCompletionChat.costStatus === 'priced'
124
138
  ? {
125
- costUsd: costUsdChat,
126
- ...(typeof metaChat.cost === 'number' ? { cost: metaChat.cost } : { cost: costUsdChat })
139
+ costUsd: costCompletionChat.cost,
140
+ ...(typeof metaChat.cost === 'number'
141
+ ? { cost: metaChat.cost }
142
+ : { cost: costCompletionChat.cost })
127
143
  }
128
- : {})
144
+ : {}),
145
+ ...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {})
129
146
  }
130
147
  };
131
148
  // Track activity success if activity was started
132
149
  if (activity) {
133
150
  try {
134
151
  await this.activityManager.logSuccess(activity, {
135
- ...(typeof costUsdChat === 'number' ? { cost: costUsdChat } : {}),
152
+ ...costCompletionChat,
136
153
  response: enhancedResponse,
137
154
  endTime: Date.now(),
138
155
  duration: Date.now() - startTime
@@ -245,7 +262,11 @@ export class AIGateway {
245
262
  // Attach parsedSnapshot to request for activity tracking
246
263
  request._parsedRequest = parsedSnapshot;
247
264
  // Merge config (modelConfig > request.config > gateway defaults)
248
- const mergedConfig = await mergeConfig(request, this.config, this.logger);
265
+ const aiTools = await this.getAiTools();
266
+ const mergedConfig = await mergeConfig(request, this.config, this.logger, {
267
+ defaultModelConfig: this.defaultModelConfig,
268
+ catalog: aiTools?.catalog ?? null
269
+ });
249
270
  request._mergedRouterConfig = mergedConfig;
250
271
  const diagnosticsMode = request.diagnostics?.mode;
251
272
  const traceEnabled = diagnosticsMode === 'trace';
@@ -523,6 +544,8 @@ export class AIGateway {
523
544
  }
524
545
  contentType = 'structured';
525
546
  parsingMethod = 'flex-md';
547
+ const outputContractKeys = resolveOutputContractFieldKeys(request);
548
+ parsedContent = await enrichParsedContentForOutputContract(parsedContent, content, outputContractKeys, this.logger);
526
549
  let tokens = extractTokenUsageFromRouterResponse(routerResponse);
527
550
  if (!(tokens.prompt || tokens.completion || tokens.total)) {
528
551
  const alt = routerResponse?.rawResponse ?? routerResponse?.raw;
@@ -532,7 +555,11 @@ export class AIGateway {
532
555
  tokens = second;
533
556
  }
534
557
  }
535
- const resolvedCostUsd = extractCostUsdFromRouterResponse(routerResponse);
558
+ const costCompletion = await resolveCostCompletionWithAiTools(routerResponse, tokens, {
559
+ mergedConfig,
560
+ calculator: aiTools?.calculator ?? null,
561
+ calculateCost: this.config.aiTools?.calculateCost
562
+ });
536
563
  const routerMetaForCost = routerResponse?.metadata || {};
537
564
  const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
538
565
  const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
@@ -551,14 +578,15 @@ export class AIGateway {
551
578
  parsingMethod,
552
579
  ...routingMetadataSlice,
553
580
  ...(effectiveModelConfig !== undefined ? { effectiveModelConfig } : {}),
554
- ...(typeof resolvedCostUsd === 'number'
581
+ ...(costCompletion.costStatus === 'priced'
555
582
  ? {
556
- costUsd: resolvedCostUsd,
583
+ costUsd: costCompletion.cost,
557
584
  ...(typeof routerMetaForCost.cost === 'number'
558
585
  ? { cost: routerMetaForCost.cost }
559
- : { cost: resolvedCostUsd })
586
+ : { cost: costCompletion.cost })
560
587
  }
561
588
  : {}),
589
+ ...(costCompletion.costStatus ? { costStatus: costCompletion.costStatus } : {}),
562
590
  ...(traceEnabled
563
591
  ? {
564
592
  requestIds: traceRequestIds,
@@ -597,7 +625,7 @@ export class AIGateway {
597
625
  usage: tokens
598
626
  };
599
627
  await this.activityManager.logSuccess(activity, {
600
- ...(typeof resolvedCostUsd === 'number' ? { cost: resolvedCostUsd } : {}),
628
+ ...costCompletion,
601
629
  response: activityResponse,
602
630
  endTime: Date.now(),
603
631
  duration: Date.now() - startTime
@@ -699,6 +727,10 @@ export class AIGateway {
699
727
  logger: this.logger
700
728
  });
701
729
  }
730
+ getAiTools() {
731
+ this._aiToolsClient ??= getAiToolsClient(this.config, this.logger);
732
+ return this._aiToolsClient;
733
+ }
702
734
  }
703
735
  function resolveRuntimeJobId(request) {
704
736
  return request.identity.jobId || request.identity.sessionId || request.aiRequestId;
package/dist/index.d.ts CHANGED
@@ -17,7 +17,12 @@ 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
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 } from './gateway-utils.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, hasNonZeroTokenUsage } from './gateway-utils.js';
21
+ export { getGatewayOperationalMode, isProdGatewayMode, resolveGatewayDefaultModel, parseModelProviderSpec, CODE_DEFAULT_MODEL } from './gateway-mode.js';
22
+ export type { GatewayOperationalMode, GatewayDefaultModelSource, DefaultModelSubstitutionReason, ResolvedGatewayDefault } from './gateway-mode.js';
23
+ export type { ActivityCostStatus, ResolvedActivityCost } from './gateway-utils.js';
24
+ export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
25
+ export type { OutputContractSpec } from './output-contract-normalizer.js';
21
26
  export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
22
27
  export type { GatewayTemplateRenderRequestSlice } from './template-render-merge.js';
23
28
  export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
package/dist/index.js CHANGED
@@ -17,7 +17,9 @@ 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 } from './gateway-utils.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, hasNonZeroTokenUsage } from './gateway-utils.js';
21
+ export { getGatewayOperationalMode, isProdGatewayMode, resolveGatewayDefaultModel, parseModelProviderSpec, CODE_DEFAULT_MODEL } from './gateway-mode.js';
22
+ export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
21
23
  export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
22
24
  export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
23
25
  // Usage tracking: UsageTracker class methods are available but consumption calculation is disabled
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
3
+ * Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
4
+ */
5
+ import type { Logxer } from '@x12i/logxer';
6
+ /** Explicit contract: field names or JSON-schema-style `properties` only. */
7
+ export type OutputContractSpec = string[] | {
8
+ properties: Record<string, unknown>;
9
+ };
10
+ /**
11
+ * Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
12
+ */
13
+ export declare function contractSpecToFieldKeys(contract: unknown): string[];
14
+ /**
15
+ * Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
16
+ */
17
+ export declare function resolveOutputContractFieldKeys(request: unknown): string[] | undefined;
18
+ /**
19
+ * Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
20
+ */
21
+ export declare function enrichParsedContentForOutputContract(parsed: unknown, rawContent: string, contractKeys: string[] | undefined, logger?: Logxer): Promise<Record<string, unknown>>;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
3
+ * Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
4
+ */
5
+ import { parseMarkdownSectionsFromContent } from './flex-md-loader.js';
6
+ import { coalesceMergedInputBucket, extractCallerInputsBag, parseLooseJsonObject } from './memory-path-resolution.js';
7
+ function isPlainObject(value) {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+ function hasMeaningfulContractValue(value) {
11
+ if (value === undefined || value === null)
12
+ return false;
13
+ if (typeof value === 'string')
14
+ return value.trim().length > 0;
15
+ if (Array.isArray(value))
16
+ return value.length > 0;
17
+ if (typeof value === 'object')
18
+ return Object.keys(value).length > 0;
19
+ return true;
20
+ }
21
+ /**
22
+ * Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
23
+ */
24
+ export function contractSpecToFieldKeys(contract) {
25
+ if (contract == null)
26
+ return [];
27
+ if (Array.isArray(contract)) {
28
+ return contract.filter((k) => typeof k === 'string' && k.trim().length > 0);
29
+ }
30
+ if (!isPlainObject(contract))
31
+ return [];
32
+ const properties = contract.properties;
33
+ if (isPlainObject(properties)) {
34
+ return Object.keys(properties);
35
+ }
36
+ return [];
37
+ }
38
+ /** Graph-engine path: `workingMemory.inputs.outputContract` or merged `input.outputContract`. */
39
+ function readExplicitOutputContractFromWorkingMemory(workingMemory) {
40
+ if (!isPlainObject(workingMemory))
41
+ return undefined;
42
+ const inputs = extractCallerInputsBag(workingMemory);
43
+ if (inputs?.outputContract !== undefined)
44
+ return inputs.outputContract;
45
+ const input = coalesceMergedInputBucket(workingMemory);
46
+ if (isPlainObject(input) && input.outputContract !== undefined) {
47
+ return input.outputContract;
48
+ }
49
+ const loose = parseLooseJsonObject(workingMemory.input);
50
+ if (loose?.outputContract !== undefined)
51
+ return loose.outputContract;
52
+ return undefined;
53
+ }
54
+ /**
55
+ * Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
56
+ */
57
+ export function resolveOutputContractFieldKeys(request) {
58
+ if (request == null || typeof request !== 'object')
59
+ return undefined;
60
+ const r = request;
61
+ const candidates = [
62
+ r.outputContract,
63
+ readExplicitOutputContractFromWorkingMemory(r.workingMemory)
64
+ ];
65
+ for (const candidate of candidates) {
66
+ const keys = contractSpecToFieldKeys(candidate);
67
+ if (keys.length > 0)
68
+ return keys;
69
+ }
70
+ return undefined;
71
+ }
72
+ function asParsedRecord(parsed) {
73
+ if (!isPlainObject(parsed))
74
+ return {};
75
+ return { ...parsed };
76
+ }
77
+ function pickAliasValue(source, key) {
78
+ if (hasMeaningfulContractValue(source[key]))
79
+ return source[key];
80
+ const spaced = key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
81
+ const title = spaced.charAt(0).toUpperCase() + spaced.slice(1);
82
+ const aliases = [
83
+ key,
84
+ key.toLowerCase(),
85
+ title,
86
+ title.replace(/\s+/g, ' '),
87
+ key.replace(/([A-Z])/g, '_$1').toLowerCase()
88
+ ];
89
+ for (const alias of aliases) {
90
+ if (hasMeaningfulContractValue(source[alias]))
91
+ return source[alias];
92
+ }
93
+ return undefined;
94
+ }
95
+ /**
96
+ * Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
97
+ */
98
+ export async function enrichParsedContentForOutputContract(parsed, rawContent, contractKeys, logger) {
99
+ const base = asParsedRecord(parsed);
100
+ if (!contractKeys?.length)
101
+ return base;
102
+ const missing = contractKeys.filter((k) => !hasMeaningfulContractValue(base[k]));
103
+ if (missing.length === 0)
104
+ return base;
105
+ const content = typeof rawContent === 'string' && rawContent.trim().length > 0
106
+ ? rawContent
107
+ : typeof base.rawText === 'string'
108
+ ? base.rawText
109
+ : '';
110
+ if (!content.trim())
111
+ return base;
112
+ const fromMarkdown = parseMarkdownSectionsFromContent(content, logger);
113
+ const merged = { ...base };
114
+ for (const key of missing) {
115
+ const value = pickAliasValue(fromMarkdown, key);
116
+ if (hasMeaningfulContractValue(value)) {
117
+ merged[key] = value;
118
+ }
119
+ }
120
+ return merged;
121
+ }
package/dist/types.d.ts CHANGED
@@ -340,6 +340,27 @@ export interface GatewayConfig extends Omit<RouterConfig, 'defaultEngine' | 'log
340
340
  openRouter?: {
341
341
  enabled?: boolean;
342
342
  };
343
+ /**
344
+ * Operational mode override (`process.env.mode` / `MODE` when omitted).
345
+ * - `prod`: unresolved models fall back to {@link AI_GATEWAY_DEFAULT_MODEL} / packaged default (with Logxer warn).
346
+ * - `dev` / `debug`: unresolved models throw {@link ModelResolutionError} from `@x12i/ai-tools`.
347
+ */
348
+ mode?: 'dev' | 'debug' | 'prod';
349
+ /**
350
+ * @x12i/ai-tools integration: catalog model resolution (request) and cost calculation (response).
351
+ */
352
+ aiTools?: {
353
+ /** @default true */
354
+ enabled?: boolean;
355
+ /** Inject Catalox; otherwise `createCataloxFromEnv()` from `@x12i/catalox/firebase`. */
356
+ catalox?: import('@x12i/catalox').Catalox;
357
+ cacheTtlMs?: number;
358
+ /** @default true */
359
+ resolveModels?: boolean;
360
+ /** @default true */
361
+ calculateCost?: boolean;
362
+ costIncludeBreakdown?: boolean;
363
+ };
343
364
  /**
344
365
  * InstructionsBlocks overrides
345
366
  * Key: block name, Value: block content
@@ -701,6 +722,14 @@ interface BaseLLMRequest extends Omit<LLMRequest, 'messages' | 'input' | 'reques
701
722
  * attach heavy diagnostic objects or raw provider payloads.
702
723
  */
703
724
  diagnostics?: DiagnosticsOptions;
725
+ /**
726
+ * Explicit output contract from graph-engine / ai-tasks: field names or `{ properties }`.
727
+ * Also accepted on `workingMemory.inputs.outputContract`. When absent, the gateway does not
728
+ * infer contract fields from other request shapes (`expectedSchema`, config, etc.).
729
+ */
730
+ outputContract?: string[] | {
731
+ properties: Record<string, unknown>;
732
+ };
704
733
  }
705
734
  /**
706
735
  * Chat request for conversational use cases
@@ -932,6 +961,12 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
932
961
  * Cost in USD (if available)
933
962
  */
934
963
  cost?: number;
964
+ /**
965
+ * Billing state for Run Analysis / Activix when usage is recorded.
966
+ * - `priced`: {@link cost} / {@link costUsd} is a finite number from the router
967
+ * - `unpriced`: usage exists but no price table / adapter cost was returned
968
+ */
969
+ costStatus?: 'priced' | 'unpriced';
935
970
  /**
936
971
  * Cost in USD (preferred stable key when the router exposes it).
937
972
  * When both are present, costUsd should mirror cost.