@x12i/ai-gateway 9.1.2 → 9.1.3

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.
@@ -133,6 +133,30 @@ function logUpstreamIdentityWarnings(logger, incomingIdentity, merged) {
133
133
  }));
134
134
  }
135
135
  }
136
+ /** Routing / generation facts from gateway response metadata for Activix `outer.metadata` on completion. */
137
+ function pickActivixCompletionRoutingMetadata(response) {
138
+ if (response == null || typeof response !== 'object')
139
+ return {};
140
+ const meta = response.metadata;
141
+ if (meta == null || typeof meta !== 'object')
142
+ return {};
143
+ const m = meta;
144
+ const out = {};
145
+ if (typeof m.modelUsed === 'string')
146
+ out.modelUsed = m.modelUsed;
147
+ if (typeof m.model === 'string')
148
+ out.model = m.model;
149
+ if (typeof m.provider === 'string')
150
+ out.provider = m.provider;
151
+ if (typeof m.maxTokensRequested === 'number')
152
+ out.maxTokensRequested = m.maxTokensRequested;
153
+ if (typeof m.region === 'string')
154
+ out.region = m.region;
155
+ if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
156
+ out.effectiveModelConfig = m.effectiveModelConfig;
157
+ }
158
+ return out;
159
+ }
136
160
  function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
137
161
  const incomingIdentity = request.identity;
138
162
  const sessionId = resolveSessionIdForRequest(request, incomingIdentity, aiRequestId);
@@ -388,33 +412,37 @@ export class ActivityManager {
388
412
  // - provider, model, temperature, maxTokens → only in config object
389
413
  // - NO response, endTime, duration (these are added via logSuccess)
390
414
  };
391
- // Config snapshot
392
- // CRITICAL: This captures the exact config sent to the router (finalRouterConfig)
393
- // This ensures activity records show the actual config that was sent, including that response_format was removed
394
- if (request.config !== undefined) {
395
- // Verify response_format is not present (for debugging)
396
- const hasResponseFormat = 'responseFormat' in request.config || 'response_format' in request.config;
415
+ // Config snapshot — prefer gateway merge output (`_mergedRouterConfig`) over raw `request.config`.
416
+ // Callers often put model only on `modelConfig`; merge happens in `mergeConfig()` before `router.invoke`.
417
+ const mergedRouterConfig = request._mergedRouterConfig;
418
+ const configSource = mergedRouterConfig != null && typeof mergedRouterConfig === 'object'
419
+ ? mergedRouterConfig
420
+ : request.config;
421
+ // CRITICAL: Capture the exact config sent to the router (finalRouterConfig)
422
+ if (configSource !== undefined) {
423
+ const hasResponseFormat = 'responseFormat' in configSource || 'response_format' in configSource;
397
424
  if (hasResponseFormat) {
398
425
  this.logger.warn('Activity tracking received config with response_format - this should not happen', {
399
426
  aiRequestId,
400
- hasResponseFormat: 'responseFormat' in request.config,
401
- hasResponse_format: 'response_format' in request.config,
402
- configKeys: Object.keys(request.config)
427
+ hasResponseFormat: 'responseFormat' in configSource,
428
+ hasResponse_format: 'response_format' in configSource,
429
+ configKeys: Object.keys(configSource)
403
430
  });
404
431
  }
405
432
  activityMetadata.config = {
406
- model: request.config.model,
407
- provider: request.config.provider || null, // Ensure provider is captured (may be null if not set)
408
- temperature: request.config.temperature,
409
- maxTokens: request.config.maxTokens,
410
- rawConfig: request.config // ✅ Captures finalRouterConfig (exact config sent to router)
433
+ model: configSource.model,
434
+ provider: configSource.provider || null,
435
+ temperature: configSource.temperature,
436
+ maxTokens: configSource.maxTokens,
437
+ rawConfig: configSource
411
438
  };
412
439
  this.logger.debug('Activity tracking config captured', {
413
440
  aiRequestId,
414
- model: request.config.model,
415
- provider: request.config.provider,
416
- hasResponseFormat: hasResponseFormat,
417
- rawConfigKeys: Object.keys(request.config).slice(0, 10)
441
+ model: configSource.model,
442
+ provider: configSource.provider,
443
+ configSource: mergedRouterConfig != null ? '_mergedRouterConfig' : 'request.config',
444
+ hasResponseFormat,
445
+ rawConfigKeys: Object.keys(configSource).slice(0, 10)
418
446
  });
419
447
  }
420
448
  // Build request object snapshots (raw = incoming; parsed = constructed messages/meta)
@@ -605,14 +633,17 @@ export class ActivityManager {
605
633
  ...(aiRequest.masterSkillActivityId && { masterSkillActivityId: aiRequest.masterSkillActivityId }),
606
634
  ...(aiRequest.masterSkillId && { masterSkillId: aiRequest.masterSkillId })
607
635
  };
608
- // Config snapshot (same as startActivity)
609
- if (request.config !== undefined) {
636
+ const mergedRouterConfigSkill = request._mergedRouterConfig;
637
+ const configSourceSkill = mergedRouterConfigSkill != null && typeof mergedRouterConfigSkill === 'object'
638
+ ? mergedRouterConfigSkill
639
+ : request.config;
640
+ if (configSourceSkill !== undefined) {
610
641
  activityMetadata.config = {
611
- model: request.config.model,
612
- provider: request.config.provider || null,
613
- temperature: request.config.temperature,
614
- maxTokens: request.config.maxTokens,
615
- rawConfig: request.config
642
+ model: configSourceSkill.model,
643
+ provider: configSourceSkill.provider || null,
644
+ temperature: configSourceSkill.temperature,
645
+ maxTokens: configSourceSkill.maxTokens,
646
+ rawConfig: configSourceSkill
616
647
  };
617
648
  }
618
649
  // Build request object snapshots (same as startActivity)
@@ -816,7 +847,7 @@ export class ActivityManager {
816
847
  response: details.response,
817
848
  outer: {
818
849
  output: details.response,
819
- metadata: {}
850
+ metadata: pickActivixCompletionRoutingMetadata(details.response)
820
851
  },
821
852
  endTime: details.endTime,
822
853
  duration: details.duration
@@ -2,7 +2,7 @@
2
2
  * Gateway Utilities Module
3
3
  * Handles utility functions
4
4
  */
5
- import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, ModelConfig } from './types.js';
5
+ import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceMergedConfig, GatewayTraceRequestIds, ModelConfig } from './types.js';
6
6
  import type { Logxer } from '@x12i/logxer';
7
7
  /**
8
8
  * Generates MD5 hash of a string
@@ -57,6 +57,10 @@ export declare function pickInvokeRoutingMetadataSlice(routerResponse: unknown,
57
57
  * Allowlisted generation profile from merged config for client introspection (no secrets, no arbitrary extras).
58
58
  */
59
59
  export declare function pickEffectiveModelConfigForMetadata(mergedConfig: unknown): Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>> | undefined;
60
+ /**
61
+ * Allowlisted snapshot of merged router config for diagnostics trace responses (no arbitrary extras).
62
+ */
63
+ export declare function pickTraceMergedRouterConfig(mergedConfig: unknown): GatewayTraceMergedConfig | undefined;
60
64
  declare const EFFECTIVE_MODEL_CONFIG_KEYS: readonly ["model", "modelId", "provider", "temperature", "maxTokens", "topP"];
61
65
  /**
62
66
  * Allowlisted generation fields from request only (before mergeConfig / flex-md).
@@ -358,6 +358,40 @@ export function pickEffectiveModelConfigForMetadata(mergedConfig) {
358
358
  }
359
359
  return Object.keys(out).length ? out : undefined;
360
360
  }
361
+ const TRACE_MERGED_ROUTER_NUMERIC_KEYS = [
362
+ 'temperature',
363
+ 'maxTokens',
364
+ 'topP',
365
+ 'frequencyPenalty',
366
+ 'presencePenalty'
367
+ ];
368
+ /**
369
+ * Allowlisted snapshot of merged router config for diagnostics trace responses (no arbitrary extras).
370
+ */
371
+ export function pickTraceMergedRouterConfig(mergedConfig) {
372
+ if (mergedConfig == null || typeof mergedConfig !== 'object')
373
+ return undefined;
374
+ const c = mergedConfig;
375
+ const out = {};
376
+ for (const k of ['model', 'modelId', 'provider']) {
377
+ const v = c[k];
378
+ if (typeof v === 'string' && v.length > 0)
379
+ out[k] = v;
380
+ }
381
+ for (const k of TRACE_MERGED_ROUTER_NUMERIC_KEYS) {
382
+ const v = c[k];
383
+ if (typeof v === 'number' && Number.isFinite(v))
384
+ out[k] = v;
385
+ }
386
+ const stop = c.stop;
387
+ if (Array.isArray(stop) && stop.every((x) => typeof x === 'string')) {
388
+ out.stop = stop;
389
+ }
390
+ else if (typeof stop === 'string' && stop.length > 0) {
391
+ out.stop = [stop];
392
+ }
393
+ return Object.keys(out).length ? out : undefined;
394
+ }
361
395
  const EFFECTIVE_MODEL_CONFIG_KEYS = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
362
396
  /**
363
397
  * Allowlisted generation fields from request only (before mergeConfig / flex-md).
package/dist/gateway.js CHANGED
@@ -8,7 +8,7 @@ import { ensureGatewayRequestIdentity } from './activity-manager.js';
8
8
  import { initializeGatewayComponents } from './gateway-config.js';
9
9
  import { buildMessages } from './message-builder.js';
10
10
  import { extractJsonFromFlexMd } from './flex-md-loader.js';
11
- import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
11
+ import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
12
12
  import { autoRegisterProviders } from './gateway-provider-auto-register.js';
13
13
  import { setGatewayLastJobId, setGatewayRuntimeClients } from './runtime-objects.js';
14
14
  import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
@@ -77,6 +77,8 @@ export class AIGateway {
77
77
  const messages = this.buildSimpleMessages(request);
78
78
  // Merge config (modelConfig > request.config > gateway defaults)
79
79
  const mergedConfig = await mergeConfig(request, this.config, this.logger);
80
+ // Activix start snapshot must match what the router receives (modelConfig-only callers omit request.config.model).
81
+ request._mergedRouterConfig = mergedConfig;
80
82
  // Lazy auto-register providers from env (OPENAI_API_KEY, etc.) so consumers don't have to call init
81
83
  if (!this._autoRegisterDone) {
82
84
  await autoRegisterProviders(this.router, this.logger);
@@ -244,6 +246,7 @@ export class AIGateway {
244
246
  request._parsedRequest = parsedSnapshot;
245
247
  // Merge config (modelConfig > request.config > gateway defaults)
246
248
  const mergedConfig = await mergeConfig(request, this.config, this.logger);
249
+ request._mergedRouterConfig = mergedConfig;
247
250
  const diagnosticsMode = request.diagnostics?.mode;
248
251
  const traceEnabled = diagnosticsMode === 'trace';
249
252
  const includeRawProviderPayload = request.diagnostics?.includeRawProviderPayload === true;
@@ -533,6 +536,7 @@ export class AIGateway {
533
536
  const routerMetaForCost = routerResponse?.metadata || {};
534
537
  const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
535
538
  const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
539
+ const traceMergedRouterSnapshot = traceEnabled ? pickTraceMergedRouterConfig(mergedConfig) : undefined;
536
540
  const enhancedResponse = {
537
541
  content: content,
538
542
  parsedContent: parsedContent,
@@ -560,7 +564,10 @@ export class AIGateway {
560
564
  requestIds: traceRequestIds,
561
565
  retryCount: traceRetryCount,
562
566
  fallbackCount: traceFallbackCount,
563
- attempts: traceAttempts
567
+ attempts: traceAttempts,
568
+ ...(traceMergedRouterSnapshot !== undefined
569
+ ? { mergedRouterConfig: traceMergedRouterSnapshot }
570
+ : {})
564
571
  }
565
572
  : {})
566
573
  }
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ 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, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions } from './types.js';
19
+ export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions } from './types.js';
20
20
  export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
21
21
  export { mergeTemplateRenderOptions } from './template-render-merge.js';
22
22
  export type { UsageTier } from './types.js';
package/dist/types.d.ts CHANGED
@@ -84,6 +84,11 @@ export type GatewayTraceAttempt = {
84
84
  */
85
85
  rawProviderPayload?: unknown;
86
86
  };
87
+ /**
88
+ * Allowlisted merged router/generation config returned in {@link EnhancedLLMResponse.metadata}
89
+ * when `diagnostics.mode === 'trace'`. Omits arbitrary extras and secrets.
90
+ */
91
+ export type GatewayTraceMergedConfig = Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP' | 'frequencyPenalty' | 'presencePenalty' | 'stop'>>;
87
92
  /**
88
93
  * Normalized observability payload attached to thrown errors from {@link AIGateway.invoke}
89
94
  * when the gateway can derive fields (merged config, partial router body on error).
@@ -954,6 +959,12 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
954
959
  * Ordered, authoritative attempts across retries and fallbacks (trace mode).
955
960
  */
956
961
  attempts?: GatewayTraceAttempt[];
962
+ /**
963
+ * Merged gateway/router generation config actually used for the invocation (after
964
+ * {@link mergeConfig}: modelConfig / request.config / defaults / flex-md maxTokens).
965
+ * Only populated when diagnostics trace mode is enabled.
966
+ */
967
+ mergedRouterConfig?: GatewayTraceMergedConfig;
957
968
  /**
958
969
  * Content type classification
959
970
  * Indicates whether content is 'string', 'object', 'array', or 'null'
@@ -137,6 +137,30 @@ function logUpstreamIdentityWarnings(logger, incomingIdentity, merged) {
137
137
  }));
138
138
  }
139
139
  }
140
+ /** Routing / generation facts from gateway response metadata for Activix `outer.metadata` on completion. */
141
+ function pickActivixCompletionRoutingMetadata(response) {
142
+ if (response == null || typeof response !== 'object')
143
+ return {};
144
+ const meta = response.metadata;
145
+ if (meta == null || typeof meta !== 'object')
146
+ return {};
147
+ const m = meta;
148
+ const out = {};
149
+ if (typeof m.modelUsed === 'string')
150
+ out.modelUsed = m.modelUsed;
151
+ if (typeof m.model === 'string')
152
+ out.model = m.model;
153
+ if (typeof m.provider === 'string')
154
+ out.provider = m.provider;
155
+ if (typeof m.maxTokensRequested === 'number')
156
+ out.maxTokensRequested = m.maxTokensRequested;
157
+ if (typeof m.region === 'string')
158
+ out.region = m.region;
159
+ if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
160
+ out.effectiveModelConfig = m.effectiveModelConfig;
161
+ }
162
+ return out;
163
+ }
140
164
  function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
141
165
  const incomingIdentity = request.identity;
142
166
  const sessionId = resolveSessionIdForRequest(request, incomingIdentity, aiRequestId);
@@ -392,33 +416,37 @@ class ActivityManager {
392
416
  // - provider, model, temperature, maxTokens → only in config object
393
417
  // - NO response, endTime, duration (these are added via logSuccess)
394
418
  };
395
- // Config snapshot
396
- // CRITICAL: This captures the exact config sent to the router (finalRouterConfig)
397
- // This ensures activity records show the actual config that was sent, including that response_format was removed
398
- if (request.config !== undefined) {
399
- // Verify response_format is not present (for debugging)
400
- const hasResponseFormat = 'responseFormat' in request.config || 'response_format' in request.config;
419
+ // Config snapshot — prefer gateway merge output (`_mergedRouterConfig`) over raw `request.config`.
420
+ // Callers often put model only on `modelConfig`; merge happens in `mergeConfig()` before `router.invoke`.
421
+ const mergedRouterConfig = request._mergedRouterConfig;
422
+ const configSource = mergedRouterConfig != null && typeof mergedRouterConfig === 'object'
423
+ ? mergedRouterConfig
424
+ : request.config;
425
+ // CRITICAL: Capture the exact config sent to the router (finalRouterConfig)
426
+ if (configSource !== undefined) {
427
+ const hasResponseFormat = 'responseFormat' in configSource || 'response_format' in configSource;
401
428
  if (hasResponseFormat) {
402
429
  this.logger.warn('Activity tracking received config with response_format - this should not happen', {
403
430
  aiRequestId,
404
- hasResponseFormat: 'responseFormat' in request.config,
405
- hasResponse_format: 'response_format' in request.config,
406
- configKeys: Object.keys(request.config)
431
+ hasResponseFormat: 'responseFormat' in configSource,
432
+ hasResponse_format: 'response_format' in configSource,
433
+ configKeys: Object.keys(configSource)
407
434
  });
408
435
  }
409
436
  activityMetadata.config = {
410
- model: request.config.model,
411
- provider: request.config.provider || null, // Ensure provider is captured (may be null if not set)
412
- temperature: request.config.temperature,
413
- maxTokens: request.config.maxTokens,
414
- rawConfig: request.config // ✅ Captures finalRouterConfig (exact config sent to router)
437
+ model: configSource.model,
438
+ provider: configSource.provider || null,
439
+ temperature: configSource.temperature,
440
+ maxTokens: configSource.maxTokens,
441
+ rawConfig: configSource
415
442
  };
416
443
  this.logger.debug('Activity tracking config captured', {
417
444
  aiRequestId,
418
- model: request.config.model,
419
- provider: request.config.provider,
420
- hasResponseFormat: hasResponseFormat,
421
- rawConfigKeys: Object.keys(request.config).slice(0, 10)
445
+ model: configSource.model,
446
+ provider: configSource.provider,
447
+ configSource: mergedRouterConfig != null ? '_mergedRouterConfig' : 'request.config',
448
+ hasResponseFormat,
449
+ rawConfigKeys: Object.keys(configSource).slice(0, 10)
422
450
  });
423
451
  }
424
452
  // Build request object snapshots (raw = incoming; parsed = constructed messages/meta)
@@ -609,14 +637,17 @@ class ActivityManager {
609
637
  ...(aiRequest.masterSkillActivityId && { masterSkillActivityId: aiRequest.masterSkillActivityId }),
610
638
  ...(aiRequest.masterSkillId && { masterSkillId: aiRequest.masterSkillId })
611
639
  };
612
- // Config snapshot (same as startActivity)
613
- if (request.config !== undefined) {
640
+ const mergedRouterConfigSkill = request._mergedRouterConfig;
641
+ const configSourceSkill = mergedRouterConfigSkill != null && typeof mergedRouterConfigSkill === 'object'
642
+ ? mergedRouterConfigSkill
643
+ : request.config;
644
+ if (configSourceSkill !== undefined) {
614
645
  activityMetadata.config = {
615
- model: request.config.model,
616
- provider: request.config.provider || null,
617
- temperature: request.config.temperature,
618
- maxTokens: request.config.maxTokens,
619
- rawConfig: request.config
646
+ model: configSourceSkill.model,
647
+ provider: configSourceSkill.provider || null,
648
+ temperature: configSourceSkill.temperature,
649
+ maxTokens: configSourceSkill.maxTokens,
650
+ rawConfig: configSourceSkill
620
651
  };
621
652
  }
622
653
  // Build request object snapshots (same as startActivity)
@@ -820,7 +851,7 @@ class ActivityManager {
820
851
  response: details.response,
821
852
  outer: {
822
853
  output: details.response,
823
- metadata: {}
854
+ metadata: pickActivixCompletionRoutingMetadata(details.response)
824
855
  },
825
856
  endTime: details.endTime,
826
857
  duration: details.duration
@@ -46,6 +46,7 @@ exports.extractTokenUsageFromRouterResponse = extractTokenUsageFromRouterRespons
46
46
  exports.extractCostUsdFromRouterResponse = extractCostUsdFromRouterResponse;
47
47
  exports.pickInvokeRoutingMetadataSlice = pickInvokeRoutingMetadataSlice;
48
48
  exports.pickEffectiveModelConfigForMetadata = pickEffectiveModelConfigForMetadata;
49
+ exports.pickTraceMergedRouterConfig = pickTraceMergedRouterConfig;
49
50
  exports.pickEffectiveModelConfigFromInvokeRequest = pickEffectiveModelConfigFromInvokeRequest;
50
51
  exports.tryExtractRouterLikePayloadFromErrorChain = tryExtractRouterLikePayloadFromErrorChain;
51
52
  exports.pickRequestIdsFromRouterLike = pickRequestIdsFromRouterLike;
@@ -408,6 +409,40 @@ function pickEffectiveModelConfigForMetadata(mergedConfig) {
408
409
  }
409
410
  return Object.keys(out).length ? out : undefined;
410
411
  }
412
+ const TRACE_MERGED_ROUTER_NUMERIC_KEYS = [
413
+ 'temperature',
414
+ 'maxTokens',
415
+ 'topP',
416
+ 'frequencyPenalty',
417
+ 'presencePenalty'
418
+ ];
419
+ /**
420
+ * Allowlisted snapshot of merged router config for diagnostics trace responses (no arbitrary extras).
421
+ */
422
+ function pickTraceMergedRouterConfig(mergedConfig) {
423
+ if (mergedConfig == null || typeof mergedConfig !== 'object')
424
+ return undefined;
425
+ const c = mergedConfig;
426
+ const out = {};
427
+ for (const k of ['model', 'modelId', 'provider']) {
428
+ const v = c[k];
429
+ if (typeof v === 'string' && v.length > 0)
430
+ out[k] = v;
431
+ }
432
+ for (const k of TRACE_MERGED_ROUTER_NUMERIC_KEYS) {
433
+ const v = c[k];
434
+ if (typeof v === 'number' && Number.isFinite(v))
435
+ out[k] = v;
436
+ }
437
+ const stop = c.stop;
438
+ if (Array.isArray(stop) && stop.every((x) => typeof x === 'string')) {
439
+ out.stop = stop;
440
+ }
441
+ else if (typeof stop === 'string' && stop.length > 0) {
442
+ out.stop = [stop];
443
+ }
444
+ return Object.keys(out).length ? out : undefined;
445
+ }
411
446
  const EFFECTIVE_MODEL_CONFIG_KEYS = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
412
447
  /**
413
448
  * Allowlisted generation fields from request only (before mergeConfig / flex-md).
@@ -2,7 +2,7 @@
2
2
  * Gateway Utilities Module
3
3
  * Handles utility functions
4
4
  */
5
- import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, ModelConfig } from './types.js';
5
+ import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceMergedConfig, GatewayTraceRequestIds, ModelConfig } from './types.js';
6
6
  import type { Logxer } from '@x12i/logxer';
7
7
  /**
8
8
  * Generates MD5 hash of a string
@@ -57,6 +57,10 @@ export declare function pickInvokeRoutingMetadataSlice(routerResponse: unknown,
57
57
  * Allowlisted generation profile from merged config for client introspection (no secrets, no arbitrary extras).
58
58
  */
59
59
  export declare function pickEffectiveModelConfigForMetadata(mergedConfig: unknown): Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>> | undefined;
60
+ /**
61
+ * Allowlisted snapshot of merged router config for diagnostics trace responses (no arbitrary extras).
62
+ */
63
+ export declare function pickTraceMergedRouterConfig(mergedConfig: unknown): GatewayTraceMergedConfig | undefined;
60
64
  declare const EFFECTIVE_MODEL_CONFIG_KEYS: readonly ["model", "modelId", "provider", "temperature", "maxTokens", "topP"];
61
65
  /**
62
66
  * Allowlisted generation fields from request only (before mergeConfig / flex-md).
@@ -80,6 +80,8 @@ class AIGateway {
80
80
  const messages = this.buildSimpleMessages(request);
81
81
  // Merge config (modelConfig > request.config > gateway defaults)
82
82
  const mergedConfig = await (0, gateway_utils_js_1.mergeConfig)(request, this.config, this.logger);
83
+ // Activix start snapshot must match what the router receives (modelConfig-only callers omit request.config.model).
84
+ request._mergedRouterConfig = mergedConfig;
83
85
  // Lazy auto-register providers from env (OPENAI_API_KEY, etc.) so consumers don't have to call init
84
86
  if (!this._autoRegisterDone) {
85
87
  await (0, gateway_provider_auto_register_js_1.autoRegisterProviders)(this.router, this.logger);
@@ -247,6 +249,7 @@ class AIGateway {
247
249
  request._parsedRequest = parsedSnapshot;
248
250
  // Merge config (modelConfig > request.config > gateway defaults)
249
251
  const mergedConfig = await (0, gateway_utils_js_1.mergeConfig)(request, this.config, this.logger);
252
+ request._mergedRouterConfig = mergedConfig;
250
253
  const diagnosticsMode = request.diagnostics?.mode;
251
254
  const traceEnabled = diagnosticsMode === 'trace';
252
255
  const includeRawProviderPayload = request.diagnostics?.includeRawProviderPayload === true;
@@ -536,6 +539,7 @@ class AIGateway {
536
539
  const routerMetaForCost = routerResponse?.metadata || {};
537
540
  const routingMetadataSlice = (0, gateway_utils_js_1.pickInvokeRoutingMetadataSlice)(routerResponse, mergedConfig);
538
541
  const effectiveModelConfig = (0, gateway_utils_js_1.pickEffectiveModelConfigForMetadata)(mergedConfig);
542
+ const traceMergedRouterSnapshot = traceEnabled ? (0, gateway_utils_js_1.pickTraceMergedRouterConfig)(mergedConfig) : undefined;
539
543
  const enhancedResponse = {
540
544
  content: content,
541
545
  parsedContent: parsedContent,
@@ -563,7 +567,10 @@ class AIGateway {
563
567
  requestIds: traceRequestIds,
564
568
  retryCount: traceRetryCount,
565
569
  fallbackCount: traceFallbackCount,
566
- attempts: traceAttempts
570
+ attempts: traceAttempts,
571
+ ...(traceMergedRouterSnapshot !== undefined
572
+ ? { mergedRouterConfig: traceMergedRouterSnapshot }
573
+ : {})
567
574
  }
568
575
  : {})
569
576
  }
@@ -16,7 +16,7 @@ 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, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions } from './types.js';
19
+ export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions } from './types.js';
20
20
  export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
21
21
  export { mergeTemplateRenderOptions } from './template-render-merge.js';
22
22
  export type { UsageTier } from './types.js';
@@ -84,6 +84,11 @@ export type GatewayTraceAttempt = {
84
84
  */
85
85
  rawProviderPayload?: unknown;
86
86
  };
87
+ /**
88
+ * Allowlisted merged router/generation config returned in {@link EnhancedLLMResponse.metadata}
89
+ * when `diagnostics.mode === 'trace'`. Omits arbitrary extras and secrets.
90
+ */
91
+ export type GatewayTraceMergedConfig = Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP' | 'frequencyPenalty' | 'presencePenalty' | 'stop'>>;
87
92
  /**
88
93
  * Normalized observability payload attached to thrown errors from {@link AIGateway.invoke}
89
94
  * when the gateway can derive fields (merged config, partial router body on error).
@@ -954,6 +959,12 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
954
959
  * Ordered, authoritative attempts across retries and fallbacks (trace mode).
955
960
  */
956
961
  attempts?: GatewayTraceAttempt[];
962
+ /**
963
+ * Merged gateway/router generation config actually used for the invocation (after
964
+ * {@link mergeConfig}: modelConfig / request.config / defaults / flex-md maxTokens).
965
+ * Only populated when diagnostics trace mode is enabled.
966
+ */
967
+ mergedRouterConfig?: GatewayTraceMergedConfig;
957
968
  /**
958
969
  * Content type classification
959
970
  * Indicates whether content is 'string', 'object', 'array', or 'null'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-gateway",
3
- "version": "9.1.2",
3
+ "version": "9.1.3",
4
4
  "description": "AI Gateway - Unified interface for LLM provider routing and management",
5
5
  "type": "module",
6
6
  "exports": {