@x12i/ai-gateway 9.1.1 → 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 { ChatRequest, GatewayConfig, 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,31 @@ 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;
64
+ declare const EFFECTIVE_MODEL_CONFIG_KEYS: readonly ["model", "modelId", "provider", "temperature", "maxTokens", "topP"];
65
+ /**
66
+ * Allowlisted generation fields from request only (before mergeConfig / flex-md).
67
+ * Priority matches mergeConfig: modelConfig overrides request.config per key.
68
+ */
69
+ export declare function pickEffectiveModelConfigFromInvokeRequest(request: Pick<AIInvokeRequest, 'config' | 'modelConfig'>): Partial<Pick<ModelConfig, (typeof EFFECTIVE_MODEL_CONFIG_KEYS)[number]>> | undefined;
70
+ /**
71
+ * Walk `error`, optional `error.cause`, and common adapter fields (`response`, `routerResponse`, …)
72
+ * to find a router-shaped object for token / correlation extraction.
73
+ */
74
+ export declare function tryExtractRouterLikePayloadFromErrorChain(error: unknown, maxDepth?: number): unknown;
75
+ export declare function pickRequestIdsFromRouterLike(gatewayAiRequestId: string | undefined, routerLike: unknown): GatewayTraceRequestIds | undefined;
76
+ export declare function buildInvokeRejectionMetadata(args: {
77
+ request: Pick<AIInvokeRequest, 'aiRequestId' | 'identity' | 'config' | 'modelConfig'>;
78
+ taskTypeId: string;
79
+ startTime: number;
80
+ mergedConfig?: unknown;
81
+ partialRouterPayload?: unknown;
82
+ gatewayAiRequestId?: string;
83
+ }): GatewayInvokeRejectionMetadata;
84
+ export declare function attachGatewayInvokeRejectionMetadata(err: Error, metadata: GatewayInvokeRejectionMetadata): void;
60
85
  /** Default JSON string length cap for Activix `content.fullResponse` when diagnostics allow storing it. */
61
86
  export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
62
87
  /**
@@ -64,3 +89,4 @@ export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
64
89
  * Non-serializable values become a small marker object instead of throwing.
65
90
  */
66
91
  export declare function capActivityFullResponsePayload(payload: unknown, maxChars?: number): unknown;
92
+ export {};
@@ -358,6 +358,148 @@ 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
+ }
395
+ const EFFECTIVE_MODEL_CONFIG_KEYS = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
396
+ /**
397
+ * Allowlisted generation fields from request only (before mergeConfig / flex-md).
398
+ * Priority matches mergeConfig: modelConfig overrides request.config per key.
399
+ */
400
+ export function pickEffectiveModelConfigFromInvokeRequest(request) {
401
+ const cfg = (request.config ?? {});
402
+ const mc = (request.modelConfig ?? {});
403
+ const out = {};
404
+ for (const k of EFFECTIVE_MODEL_CONFIG_KEYS) {
405
+ const v = mc[k] ?? cfg[k];
406
+ if (v !== undefined)
407
+ out[k] = v;
408
+ }
409
+ const modelFromId = out.model === undefined && typeof mc.modelId === 'string' ? mc.modelId : undefined;
410
+ if (modelFromId !== undefined)
411
+ out.model = modelFromId;
412
+ return Object.keys(out).length ? out : undefined;
413
+ }
414
+ function isRouterLikeEnvelope(value) {
415
+ if (value == null || typeof value !== 'object')
416
+ return false;
417
+ const r = value;
418
+ return ('metadata' in r ||
419
+ 'outputText' in r ||
420
+ 'content' in r ||
421
+ 'requestId' in r ||
422
+ 'usage' in r);
423
+ }
424
+ /**
425
+ * Walk `error`, optional `error.cause`, and common adapter fields (`response`, `routerResponse`, …)
426
+ * to find a router-shaped object for token / correlation extraction.
427
+ */
428
+ export function tryExtractRouterLikePayloadFromErrorChain(error, maxDepth = 8) {
429
+ const seen = new Set();
430
+ let cur = error;
431
+ for (let i = 0; i < maxDepth && cur != null; i++) {
432
+ if (typeof cur !== 'object')
433
+ break;
434
+ if (seen.has(cur))
435
+ break;
436
+ seen.add(cur);
437
+ const o = cur;
438
+ if (isRouterLikeEnvelope(cur))
439
+ return cur;
440
+ const nested = [o.response, o.routerResponse, o.lastResponse, o.body, o.data];
441
+ for (const n of nested) {
442
+ if (isRouterLikeEnvelope(n))
443
+ return n;
444
+ }
445
+ cur = o.cause;
446
+ }
447
+ return undefined;
448
+ }
449
+ export function pickRequestIdsFromRouterLike(gatewayAiRequestId, routerLike) {
450
+ if (typeof gatewayAiRequestId !== 'string' || gatewayAiRequestId.length === 0) {
451
+ return undefined;
452
+ }
453
+ const out = { gatewayAiRequestId };
454
+ if (routerLike == null || typeof routerLike !== 'object') {
455
+ return out;
456
+ }
457
+ const rr = routerLike;
458
+ const meta = rr.metadata != null && typeof rr.metadata === 'object' ? rr.metadata : {};
459
+ const routerRequestId = rr.requestId ?? meta.requestId;
460
+ if (typeof routerRequestId === 'string')
461
+ out.routerRequestId = routerRequestId;
462
+ if (typeof meta.providerRequestId === 'string')
463
+ out.providerRequestId = meta.providerRequestId;
464
+ if (typeof meta.openrouterRequestId === 'string')
465
+ out.openrouterRequestId = meta.openrouterRequestId;
466
+ const nested = meta.requestIds;
467
+ if (nested != null && typeof nested === 'object') {
468
+ for (const [k, v] of Object.entries(nested)) {
469
+ if (typeof v === 'string')
470
+ out[k] = v;
471
+ }
472
+ }
473
+ return out;
474
+ }
475
+ export function buildInvokeRejectionMetadata(args) {
476
+ const gid = args.gatewayAiRequestId ?? args.request.aiRequestId;
477
+ const partial = args.partialRouterPayload;
478
+ const mc = args.mergedConfig;
479
+ const routing = pickInvokeRoutingMetadataSlice(partial ?? {}, mc ?? {});
480
+ const effective = mc !== undefined
481
+ ? pickEffectiveModelConfigForMetadata(mc)
482
+ : pickEffectiveModelConfigFromInvokeRequest(args.request);
483
+ let tokens = partial !== undefined ? extractTokenUsageFromRouterResponse(partial) : undefined;
484
+ if (tokens && tokens.prompt === 0 && tokens.completion === 0 && tokens.total === 0) {
485
+ tokens = undefined;
486
+ }
487
+ const requestIds = pickRequestIdsFromRouterLike(gid, partial);
488
+ return {
489
+ aiRequestId: args.request.aiRequestId,
490
+ identity: args.request.identity,
491
+ taskTypeId: args.taskTypeId,
492
+ latencyMs: Date.now() - args.startTime,
493
+ ...routing,
494
+ ...(effective !== undefined ? { effectiveModelConfig: effective } : {}),
495
+ ...(tokens !== undefined ? { tokens } : {}),
496
+ ...(requestIds !== undefined ? { requestIds } : {}),
497
+ ...(mc === undefined ? { mergeConfigUnavailable: true } : {})
498
+ };
499
+ }
500
+ export function attachGatewayInvokeRejectionMetadata(err, metadata) {
501
+ err.metadata = metadata;
502
+ }
361
503
  /** Default JSON string length cap for Activix `content.fullResponse` when diagnostics allow storing it. */
362
504
  export const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512_000;
363
505
  /**
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 { capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice } 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);
@@ -223,6 +225,13 @@ export class AIGateway {
223
225
  failureType: 'validation-failure'
224
226
  }, startTime);
225
227
  }
228
+ const rejectMeta = buildInvokeRejectionMetadata({
229
+ request,
230
+ taskTypeId,
231
+ startTime,
232
+ gatewayAiRequestId: request.aiRequestId
233
+ });
234
+ attachGatewayInvokeRejectionMetadata(err, rejectMeta);
226
235
  // Re-throw the error so it propagates to the caller
227
236
  throw err;
228
237
  }
@@ -237,6 +246,7 @@ export class AIGateway {
237
246
  request._parsedRequest = parsedSnapshot;
238
247
  // Merge config (modelConfig > request.config > gateway defaults)
239
248
  const mergedConfig = await mergeConfig(request, this.config, this.logger);
249
+ request._mergedRouterConfig = mergedConfig;
240
250
  const diagnosticsMode = request.diagnostics?.mode;
241
251
  const traceEnabled = diagnosticsMode === 'trace';
242
252
  const includeRawProviderPayload = request.diagnostics?.includeRawProviderPayload === true;
@@ -526,6 +536,7 @@ export class AIGateway {
526
536
  const routerMetaForCost = routerResponse?.metadata || {};
527
537
  const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
528
538
  const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
539
+ const traceMergedRouterSnapshot = traceEnabled ? pickTraceMergedRouterConfig(mergedConfig) : undefined;
529
540
  const enhancedResponse = {
530
541
  content: content,
531
542
  parsedContent: parsedContent,
@@ -553,7 +564,10 @@ export class AIGateway {
553
564
  requestIds: traceRequestIds,
554
565
  retryCount: traceRetryCount,
555
566
  fallbackCount: traceFallbackCount,
556
- attempts: traceAttempts
567
+ attempts: traceAttempts,
568
+ ...(traceMergedRouterSnapshot !== undefined
569
+ ? { mergedRouterConfig: traceMergedRouterSnapshot }
570
+ : {})
557
571
  }
558
572
  : {})
559
573
  }
@@ -611,8 +625,21 @@ export class AIGateway {
611
625
  }
612
626
  catch (error) {
613
627
  const err = error instanceof Error ? error : new Error(String(error));
628
+ const partial = tryExtractRouterLikePayloadFromErrorChain(err);
629
+ const rejectMeta = buildInvokeRejectionMetadata({
630
+ request,
631
+ taskTypeId,
632
+ startTime,
633
+ mergedConfig,
634
+ partialRouterPayload: partial,
635
+ gatewayAiRequestId: request.aiRequestId
636
+ });
637
+ attachGatewayInvokeRejectionMetadata(err, rejectMeta);
614
638
  if (err.message.includes(NO_PROVIDER_ERROR)) {
615
- throw new Error(err.message + NO_PROVIDER_HINT);
639
+ const wrapped = new Error(err.message + NO_PROVIDER_HINT);
640
+ wrapped.cause = err;
641
+ attachGatewayInvokeRejectionMetadata(wrapped, rejectMeta);
642
+ throw wrapped;
616
643
  }
617
644
  throw err;
618
645
  }
package/dist/index.d.ts CHANGED
@@ -16,7 +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, 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
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
20
21
  export { mergeTemplateRenderOptions } from './template-render-merge.js';
21
22
  export type { UsageTier } from './types.js';
22
23
  export { Activix } from '@x12i/activix';
package/dist/index.js CHANGED
@@ -17,6 +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 } from './gateway-utils.js';
20
21
  export { mergeTemplateRenderOptions } from './template-render-merge.js';
21
22
  // Usage tracking: UsageTracker class methods are available but consumption calculation is disabled
22
23
  // (x-models was previously used for RPM/TPM tracking but is no longer integrated)
package/dist/types.d.ts CHANGED
@@ -84,6 +84,39 @@ 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'>>;
92
+ /**
93
+ * Normalized observability payload attached to thrown errors from {@link AIGateway.invoke}
94
+ * when the gateway can derive fields (merged config, partial router body on error).
95
+ * SDKs should read `(error as Error & { metadata?: GatewayInvokeRejectionMetadata }).metadata`
96
+ * (see docs/AI_GATEWAY_INVOKE_EXECUTION_METADATA.md).
97
+ */
98
+ export type GatewayInvokeRejectionMetadata = {
99
+ aiRequestId?: string;
100
+ identity?: ActivityIdentity;
101
+ taskTypeId?: string;
102
+ latencyMs?: number;
103
+ tokens?: {
104
+ prompt: number;
105
+ completion: number;
106
+ total: number;
107
+ };
108
+ provider?: string;
109
+ modelUsed?: string;
110
+ maxTokensRequested?: number;
111
+ region?: string;
112
+ effectiveModelConfig?: Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>>;
113
+ requestIds?: GatewayTraceRequestIds;
114
+ /**
115
+ * True when {@link mergeConfig} did not run (e.g. message-building threw first).
116
+ * Routing facts may only reflect request.config / modelConfig, not flex-md defaults.
117
+ */
118
+ mergeConfigUnavailable?: true;
119
+ };
87
120
  /**
88
121
  * Identity object used for activity linkage.
89
122
  * On gateway requests/responses it lives on `identity`. When activity tracking persists via Activix v5+,
@@ -926,6 +959,12 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
926
959
  * Ordered, authoritative attempts across retries and fallbacks (trace mode).
927
960
  */
928
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;
929
968
  /**
930
969
  * Content type classification
931
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,12 @@ exports.extractTokenUsageFromRouterResponse = extractTokenUsageFromRouterRespons
46
46
  exports.extractCostUsdFromRouterResponse = extractCostUsdFromRouterResponse;
47
47
  exports.pickInvokeRoutingMetadataSlice = pickInvokeRoutingMetadataSlice;
48
48
  exports.pickEffectiveModelConfigForMetadata = pickEffectiveModelConfigForMetadata;
49
+ exports.pickTraceMergedRouterConfig = pickTraceMergedRouterConfig;
50
+ exports.pickEffectiveModelConfigFromInvokeRequest = pickEffectiveModelConfigFromInvokeRequest;
51
+ exports.tryExtractRouterLikePayloadFromErrorChain = tryExtractRouterLikePayloadFromErrorChain;
52
+ exports.pickRequestIdsFromRouterLike = pickRequestIdsFromRouterLike;
53
+ exports.buildInvokeRejectionMetadata = buildInvokeRejectionMetadata;
54
+ exports.attachGatewayInvokeRejectionMetadata = attachGatewayInvokeRejectionMetadata;
49
55
  exports.capActivityFullResponsePayload = capActivityFullResponsePayload;
50
56
  const crypto = __importStar(require("crypto"));
51
57
  const gateway_instructions_js_1 = require("./gateway-instructions.cjs");
@@ -403,6 +409,148 @@ function pickEffectiveModelConfigForMetadata(mergedConfig) {
403
409
  }
404
410
  return Object.keys(out).length ? out : undefined;
405
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
+ }
446
+ const EFFECTIVE_MODEL_CONFIG_KEYS = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
447
+ /**
448
+ * Allowlisted generation fields from request only (before mergeConfig / flex-md).
449
+ * Priority matches mergeConfig: modelConfig overrides request.config per key.
450
+ */
451
+ function pickEffectiveModelConfigFromInvokeRequest(request) {
452
+ const cfg = (request.config ?? {});
453
+ const mc = (request.modelConfig ?? {});
454
+ const out = {};
455
+ for (const k of EFFECTIVE_MODEL_CONFIG_KEYS) {
456
+ const v = mc[k] ?? cfg[k];
457
+ if (v !== undefined)
458
+ out[k] = v;
459
+ }
460
+ const modelFromId = out.model === undefined && typeof mc.modelId === 'string' ? mc.modelId : undefined;
461
+ if (modelFromId !== undefined)
462
+ out.model = modelFromId;
463
+ return Object.keys(out).length ? out : undefined;
464
+ }
465
+ function isRouterLikeEnvelope(value) {
466
+ if (value == null || typeof value !== 'object')
467
+ return false;
468
+ const r = value;
469
+ return ('metadata' in r ||
470
+ 'outputText' in r ||
471
+ 'content' in r ||
472
+ 'requestId' in r ||
473
+ 'usage' in r);
474
+ }
475
+ /**
476
+ * Walk `error`, optional `error.cause`, and common adapter fields (`response`, `routerResponse`, …)
477
+ * to find a router-shaped object for token / correlation extraction.
478
+ */
479
+ function tryExtractRouterLikePayloadFromErrorChain(error, maxDepth = 8) {
480
+ const seen = new Set();
481
+ let cur = error;
482
+ for (let i = 0; i < maxDepth && cur != null; i++) {
483
+ if (typeof cur !== 'object')
484
+ break;
485
+ if (seen.has(cur))
486
+ break;
487
+ seen.add(cur);
488
+ const o = cur;
489
+ if (isRouterLikeEnvelope(cur))
490
+ return cur;
491
+ const nested = [o.response, o.routerResponse, o.lastResponse, o.body, o.data];
492
+ for (const n of nested) {
493
+ if (isRouterLikeEnvelope(n))
494
+ return n;
495
+ }
496
+ cur = o.cause;
497
+ }
498
+ return undefined;
499
+ }
500
+ function pickRequestIdsFromRouterLike(gatewayAiRequestId, routerLike) {
501
+ if (typeof gatewayAiRequestId !== 'string' || gatewayAiRequestId.length === 0) {
502
+ return undefined;
503
+ }
504
+ const out = { gatewayAiRequestId };
505
+ if (routerLike == null || typeof routerLike !== 'object') {
506
+ return out;
507
+ }
508
+ const rr = routerLike;
509
+ const meta = rr.metadata != null && typeof rr.metadata === 'object' ? rr.metadata : {};
510
+ const routerRequestId = rr.requestId ?? meta.requestId;
511
+ if (typeof routerRequestId === 'string')
512
+ out.routerRequestId = routerRequestId;
513
+ if (typeof meta.providerRequestId === 'string')
514
+ out.providerRequestId = meta.providerRequestId;
515
+ if (typeof meta.openrouterRequestId === 'string')
516
+ out.openrouterRequestId = meta.openrouterRequestId;
517
+ const nested = meta.requestIds;
518
+ if (nested != null && typeof nested === 'object') {
519
+ for (const [k, v] of Object.entries(nested)) {
520
+ if (typeof v === 'string')
521
+ out[k] = v;
522
+ }
523
+ }
524
+ return out;
525
+ }
526
+ function buildInvokeRejectionMetadata(args) {
527
+ const gid = args.gatewayAiRequestId ?? args.request.aiRequestId;
528
+ const partial = args.partialRouterPayload;
529
+ const mc = args.mergedConfig;
530
+ const routing = pickInvokeRoutingMetadataSlice(partial ?? {}, mc ?? {});
531
+ const effective = mc !== undefined
532
+ ? pickEffectiveModelConfigForMetadata(mc)
533
+ : pickEffectiveModelConfigFromInvokeRequest(args.request);
534
+ let tokens = partial !== undefined ? extractTokenUsageFromRouterResponse(partial) : undefined;
535
+ if (tokens && tokens.prompt === 0 && tokens.completion === 0 && tokens.total === 0) {
536
+ tokens = undefined;
537
+ }
538
+ const requestIds = pickRequestIdsFromRouterLike(gid, partial);
539
+ return {
540
+ aiRequestId: args.request.aiRequestId,
541
+ identity: args.request.identity,
542
+ taskTypeId: args.taskTypeId,
543
+ latencyMs: Date.now() - args.startTime,
544
+ ...routing,
545
+ ...(effective !== undefined ? { effectiveModelConfig: effective } : {}),
546
+ ...(tokens !== undefined ? { tokens } : {}),
547
+ ...(requestIds !== undefined ? { requestIds } : {}),
548
+ ...(mc === undefined ? { mergeConfigUnavailable: true } : {})
549
+ };
550
+ }
551
+ function attachGatewayInvokeRejectionMetadata(err, metadata) {
552
+ err.metadata = metadata;
553
+ }
406
554
  /** Default JSON string length cap for Activix `content.fullResponse` when diagnostics allow storing it. */
407
555
  exports.DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512_000;
408
556
  /**
@@ -2,7 +2,7 @@
2
2
  * Gateway Utilities Module
3
3
  * Handles utility functions
4
4
  */
5
- import type { ChatRequest, GatewayConfig, 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,31 @@ 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;
64
+ declare const EFFECTIVE_MODEL_CONFIG_KEYS: readonly ["model", "modelId", "provider", "temperature", "maxTokens", "topP"];
65
+ /**
66
+ * Allowlisted generation fields from request only (before mergeConfig / flex-md).
67
+ * Priority matches mergeConfig: modelConfig overrides request.config per key.
68
+ */
69
+ export declare function pickEffectiveModelConfigFromInvokeRequest(request: Pick<AIInvokeRequest, 'config' | 'modelConfig'>): Partial<Pick<ModelConfig, (typeof EFFECTIVE_MODEL_CONFIG_KEYS)[number]>> | undefined;
70
+ /**
71
+ * Walk `error`, optional `error.cause`, and common adapter fields (`response`, `routerResponse`, …)
72
+ * to find a router-shaped object for token / correlation extraction.
73
+ */
74
+ export declare function tryExtractRouterLikePayloadFromErrorChain(error: unknown, maxDepth?: number): unknown;
75
+ export declare function pickRequestIdsFromRouterLike(gatewayAiRequestId: string | undefined, routerLike: unknown): GatewayTraceRequestIds | undefined;
76
+ export declare function buildInvokeRejectionMetadata(args: {
77
+ request: Pick<AIInvokeRequest, 'aiRequestId' | 'identity' | 'config' | 'modelConfig'>;
78
+ taskTypeId: string;
79
+ startTime: number;
80
+ mergedConfig?: unknown;
81
+ partialRouterPayload?: unknown;
82
+ gatewayAiRequestId?: string;
83
+ }): GatewayInvokeRejectionMetadata;
84
+ export declare function attachGatewayInvokeRejectionMetadata(err: Error, metadata: GatewayInvokeRejectionMetadata): void;
60
85
  /** Default JSON string length cap for Activix `content.fullResponse` when diagnostics allow storing it. */
61
86
  export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
62
87
  /**
@@ -64,3 +89,4 @@ export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
64
89
  * Non-serializable values become a small marker object instead of throwing.
65
90
  */
66
91
  export declare function capActivityFullResponsePayload(payload: unknown, maxChars?: number): unknown;
92
+ export {};
@@ -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);
@@ -226,6 +228,13 @@ class AIGateway {
226
228
  failureType: 'validation-failure'
227
229
  }, startTime);
228
230
  }
231
+ const rejectMeta = (0, gateway_utils_js_1.buildInvokeRejectionMetadata)({
232
+ request,
233
+ taskTypeId,
234
+ startTime,
235
+ gatewayAiRequestId: request.aiRequestId
236
+ });
237
+ (0, gateway_utils_js_1.attachGatewayInvokeRejectionMetadata)(err, rejectMeta);
229
238
  // Re-throw the error so it propagates to the caller
230
239
  throw err;
231
240
  }
@@ -240,6 +249,7 @@ class AIGateway {
240
249
  request._parsedRequest = parsedSnapshot;
241
250
  // Merge config (modelConfig > request.config > gateway defaults)
242
251
  const mergedConfig = await (0, gateway_utils_js_1.mergeConfig)(request, this.config, this.logger);
252
+ request._mergedRouterConfig = mergedConfig;
243
253
  const diagnosticsMode = request.diagnostics?.mode;
244
254
  const traceEnabled = diagnosticsMode === 'trace';
245
255
  const includeRawProviderPayload = request.diagnostics?.includeRawProviderPayload === true;
@@ -529,6 +539,7 @@ class AIGateway {
529
539
  const routerMetaForCost = routerResponse?.metadata || {};
530
540
  const routingMetadataSlice = (0, gateway_utils_js_1.pickInvokeRoutingMetadataSlice)(routerResponse, mergedConfig);
531
541
  const effectiveModelConfig = (0, gateway_utils_js_1.pickEffectiveModelConfigForMetadata)(mergedConfig);
542
+ const traceMergedRouterSnapshot = traceEnabled ? (0, gateway_utils_js_1.pickTraceMergedRouterConfig)(mergedConfig) : undefined;
532
543
  const enhancedResponse = {
533
544
  content: content,
534
545
  parsedContent: parsedContent,
@@ -556,7 +567,10 @@ class AIGateway {
556
567
  requestIds: traceRequestIds,
557
568
  retryCount: traceRetryCount,
558
569
  fallbackCount: traceFallbackCount,
559
- attempts: traceAttempts
570
+ attempts: traceAttempts,
571
+ ...(traceMergedRouterSnapshot !== undefined
572
+ ? { mergedRouterConfig: traceMergedRouterSnapshot }
573
+ : {})
560
574
  }
561
575
  : {})
562
576
  }
@@ -614,8 +628,21 @@ class AIGateway {
614
628
  }
615
629
  catch (error) {
616
630
  const err = error instanceof Error ? error : new Error(String(error));
631
+ const partial = (0, gateway_utils_js_1.tryExtractRouterLikePayloadFromErrorChain)(err);
632
+ const rejectMeta = (0, gateway_utils_js_1.buildInvokeRejectionMetadata)({
633
+ request,
634
+ taskTypeId,
635
+ startTime,
636
+ mergedConfig,
637
+ partialRouterPayload: partial,
638
+ gatewayAiRequestId: request.aiRequestId
639
+ });
640
+ (0, gateway_utils_js_1.attachGatewayInvokeRejectionMetadata)(err, rejectMeta);
617
641
  if (err.message.includes(NO_PROVIDER_ERROR)) {
618
- throw new Error(err.message + NO_PROVIDER_HINT);
642
+ const wrapped = new Error(err.message + NO_PROVIDER_HINT);
643
+ wrapped.cause = err;
644
+ (0, gateway_utils_js_1.attachGatewayInvokeRejectionMetadata)(wrapped, rejectMeta);
645
+ throw wrapped;
619
646
  }
620
647
  throw err;
621
648
  }
@@ -21,7 +21,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
21
21
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.resetObjectTypesLibrary = exports.getObjectTypesLibrary = exports.initializeObjectTypesLibrary = exports.getObjectTypesForAgent = exports.getObjectType = exports.OBJECT_TYPES_LIBRARY = exports.assertValidAIRequest = exports.formatDiagnostic = exports.runValidationTests = exports.createValidationTestCases = exports.createTestAIRequest = exports.supportsJSONMode = exports.diagnoseResponse = exports.diagnoseRequest = exports.validateResponse = exports.extractJSON = exports.validateJSON = exports.validateAIRequest = exports.DEFAULT_RATE_LIMIT_ENABLED = exports.DEFAULT_RATE_LIMIT_MIN_INTERVAL_MS = exports.GatewayRateLimiter = exports.runtimeObjects = exports.DebugLogAbstract = exports.createLogxer = exports.gatewayLogDebug = exports.withActivityIdentity = exports.activityIdentityToLogMeta = exports.ensureGatewayRequestIdentity = exports.ActivityManager = exports.Activix = exports.mergeTemplateRenderOptions = exports.autoRegisterProviders = exports.InstructionBackendError = exports.InstructionNotFoundError = exports.AIGateway = exports.FallbackExhaustedError = exports.ProviderNotFoundError = exports.createRouterFromConfig = exports.createRouter = exports.LLMProviderRouter = void 0;
24
+ exports.resetObjectTypesLibrary = exports.getObjectTypesLibrary = exports.initializeObjectTypesLibrary = exports.getObjectTypesForAgent = exports.getObjectType = exports.OBJECT_TYPES_LIBRARY = exports.assertValidAIRequest = exports.formatDiagnostic = exports.runValidationTests = exports.createValidationTestCases = exports.createTestAIRequest = exports.supportsJSONMode = exports.diagnoseResponse = exports.diagnoseRequest = exports.validateResponse = exports.extractJSON = exports.validateJSON = exports.validateAIRequest = exports.DEFAULT_RATE_LIMIT_ENABLED = exports.DEFAULT_RATE_LIMIT_MIN_INTERVAL_MS = exports.GatewayRateLimiter = exports.runtimeObjects = exports.DebugLogAbstract = exports.createLogxer = exports.gatewayLogDebug = exports.withActivityIdentity = exports.activityIdentityToLogMeta = exports.ensureGatewayRequestIdentity = exports.ActivityManager = exports.Activix = exports.mergeTemplateRenderOptions = exports.pickRequestIdsFromRouterLike = exports.tryExtractRouterLikePayloadFromErrorChain = exports.buildInvokeRejectionMetadata = exports.attachGatewayInvokeRejectionMetadata = exports.autoRegisterProviders = exports.InstructionBackendError = exports.InstructionNotFoundError = exports.AIGateway = exports.FallbackExhaustedError = exports.ProviderNotFoundError = exports.createRouterFromConfig = exports.createRouter = exports.LLMProviderRouter = void 0;
25
25
  // Re-export router class and types (base functionality)
26
26
  var ai_providers_router_1 = require("@x12i/ai-providers-router");
27
27
  Object.defineProperty(exports, "LLMProviderRouter", { enumerable: true, get: function () { return ai_providers_router_1.LLMProviderRouter; } });
@@ -43,6 +43,11 @@ Object.defineProperty(exports, "InstructionNotFoundError", { enumerable: true, g
43
43
  Object.defineProperty(exports, "InstructionBackendError", { enumerable: true, get: function () { return instruction_errors_js_1.InstructionBackendError; } });
44
44
  var gateway_provider_auto_register_js_1 = require("./gateway-provider-auto-register.cjs");
45
45
  Object.defineProperty(exports, "autoRegisterProviders", { enumerable: true, get: function () { return gateway_provider_auto_register_js_1.autoRegisterProviders; } });
46
+ var gateway_utils_js_1 = require("./gateway-utils.cjs");
47
+ Object.defineProperty(exports, "attachGatewayInvokeRejectionMetadata", { enumerable: true, get: function () { return gateway_utils_js_1.attachGatewayInvokeRejectionMetadata; } });
48
+ Object.defineProperty(exports, "buildInvokeRejectionMetadata", { enumerable: true, get: function () { return gateway_utils_js_1.buildInvokeRejectionMetadata; } });
49
+ Object.defineProperty(exports, "tryExtractRouterLikePayloadFromErrorChain", { enumerable: true, get: function () { return gateway_utils_js_1.tryExtractRouterLikePayloadFromErrorChain; } });
50
+ Object.defineProperty(exports, "pickRequestIdsFromRouterLike", { enumerable: true, get: function () { return gateway_utils_js_1.pickRequestIdsFromRouterLike; } });
46
51
  var template_render_merge_js_1 = require("./template-render-merge.cjs");
47
52
  Object.defineProperty(exports, "mergeTemplateRenderOptions", { enumerable: true, get: function () { return template_render_merge_js_1.mergeTemplateRenderOptions; } });
48
53
  // Usage tracking: UsageTracker class methods are available but consumption calculation is disabled
@@ -16,7 +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, 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
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
20
21
  export { mergeTemplateRenderOptions } from './template-render-merge.js';
21
22
  export type { UsageTier } from './types.js';
22
23
  export { Activix } from '@x12i/activix';
@@ -84,6 +84,39 @@ 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'>>;
92
+ /**
93
+ * Normalized observability payload attached to thrown errors from {@link AIGateway.invoke}
94
+ * when the gateway can derive fields (merged config, partial router body on error).
95
+ * SDKs should read `(error as Error & { metadata?: GatewayInvokeRejectionMetadata }).metadata`
96
+ * (see docs/AI_GATEWAY_INVOKE_EXECUTION_METADATA.md).
97
+ */
98
+ export type GatewayInvokeRejectionMetadata = {
99
+ aiRequestId?: string;
100
+ identity?: ActivityIdentity;
101
+ taskTypeId?: string;
102
+ latencyMs?: number;
103
+ tokens?: {
104
+ prompt: number;
105
+ completion: number;
106
+ total: number;
107
+ };
108
+ provider?: string;
109
+ modelUsed?: string;
110
+ maxTokensRequested?: number;
111
+ region?: string;
112
+ effectiveModelConfig?: Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>>;
113
+ requestIds?: GatewayTraceRequestIds;
114
+ /**
115
+ * True when {@link mergeConfig} did not run (e.g. message-building threw first).
116
+ * Routing facts may only reflect request.config / modelConfig, not flex-md defaults.
117
+ */
118
+ mergeConfigUnavailable?: true;
119
+ };
87
120
  /**
88
121
  * Identity object used for activity linkage.
89
122
  * On gateway requests/responses it lives on `identity`. When activity tracking persists via Activix v5+,
@@ -926,6 +959,12 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
926
959
  * Ordered, authoritative attempts across retries and fallbacks (trace mode).
927
960
  */
928
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;
929
968
  /**
930
969
  * Content type classification
931
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.1",
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": {