@x12i/ai-gateway 9.1.0 → 9.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  * Gateway Utilities Module
3
3
  * Handles utility functions
4
4
  */
5
- import type { ChatRequest, GatewayConfig } from './types.js';
5
+ import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, ModelConfig } from './types.js';
6
6
  import type { Logxer } from '@x12i/logxer';
7
7
  /**
8
8
  * Generates MD5 hash of a string
@@ -43,6 +43,41 @@ export declare function extractTokenUsageFromRouterResponse(routerResponse: unkn
43
43
  * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
44
44
  */
45
45
  export declare function extractCostUsdFromRouterResponse(routerResponse: unknown): number | undefined;
46
+ /**
47
+ * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
48
+ * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
49
+ */
50
+ export declare function pickInvokeRoutingMetadataSlice(routerResponse: unknown, mergedConfig: unknown): Partial<{
51
+ provider: string;
52
+ modelUsed: string;
53
+ maxTokensRequested: number;
54
+ region: string;
55
+ }>;
56
+ /**
57
+ * Allowlisted generation profile from merged config for client introspection (no secrets, no arbitrary extras).
58
+ */
59
+ export declare function pickEffectiveModelConfigForMetadata(mergedConfig: unknown): Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>> | undefined;
60
+ declare const EFFECTIVE_MODEL_CONFIG_KEYS: readonly ["model", "modelId", "provider", "temperature", "maxTokens", "topP"];
61
+ /**
62
+ * Allowlisted generation fields from request only (before mergeConfig / flex-md).
63
+ * Priority matches mergeConfig: modelConfig overrides request.config per key.
64
+ */
65
+ export declare function pickEffectiveModelConfigFromInvokeRequest(request: Pick<AIInvokeRequest, 'config' | 'modelConfig'>): Partial<Pick<ModelConfig, (typeof EFFECTIVE_MODEL_CONFIG_KEYS)[number]>> | undefined;
66
+ /**
67
+ * Walk `error`, optional `error.cause`, and common adapter fields (`response`, `routerResponse`, …)
68
+ * to find a router-shaped object for token / correlation extraction.
69
+ */
70
+ export declare function tryExtractRouterLikePayloadFromErrorChain(error: unknown, maxDepth?: number): unknown;
71
+ export declare function pickRequestIdsFromRouterLike(gatewayAiRequestId: string | undefined, routerLike: unknown): GatewayTraceRequestIds | undefined;
72
+ export declare function buildInvokeRejectionMetadata(args: {
73
+ request: Pick<AIInvokeRequest, 'aiRequestId' | 'identity' | 'config' | 'modelConfig'>;
74
+ taskTypeId: string;
75
+ startTime: number;
76
+ mergedConfig?: unknown;
77
+ partialRouterPayload?: unknown;
78
+ gatewayAiRequestId?: string;
79
+ }): GatewayInvokeRejectionMetadata;
80
+ export declare function attachGatewayInvokeRejectionMetadata(err: Error, metadata: GatewayInvokeRejectionMetadata): void;
46
81
  /** Default JSON string length cap for Activix `content.fullResponse` when diagnostics allow storing it. */
47
82
  export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
48
83
  /**
@@ -50,3 +85,4 @@ export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
50
85
  * Non-serializable values become a small marker object instead of throwing.
51
86
  */
52
87
  export declare function capActivityFullResponsePayload(payload: unknown, maxChars?: number): unknown;
88
+ export {};
@@ -315,6 +315,157 @@ export function extractCostUsdFromRouterResponse(routerResponse) {
315
315
  }
316
316
  return undefined;
317
317
  }
318
+ /**
319
+ * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
320
+ * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
321
+ */
322
+ export function pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig) {
323
+ const rr = routerResponse != null && typeof routerResponse === 'object' ? routerResponse : {};
324
+ const meta = rr.metadata != null && typeof rr.metadata === 'object' ? rr.metadata : {};
325
+ const cfg = mergedConfig != null && typeof mergedConfig === 'object' ? mergedConfig : {};
326
+ const provider = meta.provider || rr.provider || cfg.provider;
327
+ const modelUsed = meta.modelUsed || meta.model || rr.model || cfg.model;
328
+ const maxTokensRequested = typeof meta.maxTokensRequested === 'number'
329
+ ? meta.maxTokensRequested
330
+ : typeof cfg.maxTokens === 'number'
331
+ ? cfg.maxTokens
332
+ : undefined;
333
+ const region = typeof meta.region === 'string' ? meta.region : undefined;
334
+ const out = {};
335
+ if (provider !== undefined && provider !== null)
336
+ out.provider = provider;
337
+ if (modelUsed !== undefined && modelUsed !== null)
338
+ out.modelUsed = modelUsed;
339
+ if (maxTokensRequested !== undefined)
340
+ out.maxTokensRequested = maxTokensRequested;
341
+ if (region !== undefined)
342
+ out.region = region;
343
+ return out;
344
+ }
345
+ /**
346
+ * Allowlisted generation profile from merged config for client introspection (no secrets, no arbitrary extras).
347
+ */
348
+ export function pickEffectiveModelConfigForMetadata(mergedConfig) {
349
+ if (mergedConfig == null || typeof mergedConfig !== 'object')
350
+ return undefined;
351
+ const c = mergedConfig;
352
+ const keys = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
353
+ const out = {};
354
+ for (const k of keys) {
355
+ const v = c[k];
356
+ if (v !== undefined)
357
+ out[k] = v;
358
+ }
359
+ return Object.keys(out).length ? out : undefined;
360
+ }
361
+ const EFFECTIVE_MODEL_CONFIG_KEYS = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
362
+ /**
363
+ * Allowlisted generation fields from request only (before mergeConfig / flex-md).
364
+ * Priority matches mergeConfig: modelConfig overrides request.config per key.
365
+ */
366
+ export function pickEffectiveModelConfigFromInvokeRequest(request) {
367
+ const cfg = (request.config ?? {});
368
+ const mc = (request.modelConfig ?? {});
369
+ const out = {};
370
+ for (const k of EFFECTIVE_MODEL_CONFIG_KEYS) {
371
+ const v = mc[k] ?? cfg[k];
372
+ if (v !== undefined)
373
+ out[k] = v;
374
+ }
375
+ const modelFromId = out.model === undefined && typeof mc.modelId === 'string' ? mc.modelId : undefined;
376
+ if (modelFromId !== undefined)
377
+ out.model = modelFromId;
378
+ return Object.keys(out).length ? out : undefined;
379
+ }
380
+ function isRouterLikeEnvelope(value) {
381
+ if (value == null || typeof value !== 'object')
382
+ return false;
383
+ const r = value;
384
+ return ('metadata' in r ||
385
+ 'outputText' in r ||
386
+ 'content' in r ||
387
+ 'requestId' in r ||
388
+ 'usage' in r);
389
+ }
390
+ /**
391
+ * Walk `error`, optional `error.cause`, and common adapter fields (`response`, `routerResponse`, …)
392
+ * to find a router-shaped object for token / correlation extraction.
393
+ */
394
+ export function tryExtractRouterLikePayloadFromErrorChain(error, maxDepth = 8) {
395
+ const seen = new Set();
396
+ let cur = error;
397
+ for (let i = 0; i < maxDepth && cur != null; i++) {
398
+ if (typeof cur !== 'object')
399
+ break;
400
+ if (seen.has(cur))
401
+ break;
402
+ seen.add(cur);
403
+ const o = cur;
404
+ if (isRouterLikeEnvelope(cur))
405
+ return cur;
406
+ const nested = [o.response, o.routerResponse, o.lastResponse, o.body, o.data];
407
+ for (const n of nested) {
408
+ if (isRouterLikeEnvelope(n))
409
+ return n;
410
+ }
411
+ cur = o.cause;
412
+ }
413
+ return undefined;
414
+ }
415
+ export function pickRequestIdsFromRouterLike(gatewayAiRequestId, routerLike) {
416
+ if (typeof gatewayAiRequestId !== 'string' || gatewayAiRequestId.length === 0) {
417
+ return undefined;
418
+ }
419
+ const out = { gatewayAiRequestId };
420
+ if (routerLike == null || typeof routerLike !== 'object') {
421
+ return out;
422
+ }
423
+ const rr = routerLike;
424
+ const meta = rr.metadata != null && typeof rr.metadata === 'object' ? rr.metadata : {};
425
+ const routerRequestId = rr.requestId ?? meta.requestId;
426
+ if (typeof routerRequestId === 'string')
427
+ out.routerRequestId = routerRequestId;
428
+ if (typeof meta.providerRequestId === 'string')
429
+ out.providerRequestId = meta.providerRequestId;
430
+ if (typeof meta.openrouterRequestId === 'string')
431
+ out.openrouterRequestId = meta.openrouterRequestId;
432
+ const nested = meta.requestIds;
433
+ if (nested != null && typeof nested === 'object') {
434
+ for (const [k, v] of Object.entries(nested)) {
435
+ if (typeof v === 'string')
436
+ out[k] = v;
437
+ }
438
+ }
439
+ return out;
440
+ }
441
+ export function buildInvokeRejectionMetadata(args) {
442
+ const gid = args.gatewayAiRequestId ?? args.request.aiRequestId;
443
+ const partial = args.partialRouterPayload;
444
+ const mc = args.mergedConfig;
445
+ const routing = pickInvokeRoutingMetadataSlice(partial ?? {}, mc ?? {});
446
+ const effective = mc !== undefined
447
+ ? pickEffectiveModelConfigForMetadata(mc)
448
+ : pickEffectiveModelConfigFromInvokeRequest(args.request);
449
+ let tokens = partial !== undefined ? extractTokenUsageFromRouterResponse(partial) : undefined;
450
+ if (tokens && tokens.prompt === 0 && tokens.completion === 0 && tokens.total === 0) {
451
+ tokens = undefined;
452
+ }
453
+ const requestIds = pickRequestIdsFromRouterLike(gid, partial);
454
+ return {
455
+ aiRequestId: args.request.aiRequestId,
456
+ identity: args.request.identity,
457
+ taskTypeId: args.taskTypeId,
458
+ latencyMs: Date.now() - args.startTime,
459
+ ...routing,
460
+ ...(effective !== undefined ? { effectiveModelConfig: effective } : {}),
461
+ ...(tokens !== undefined ? { tokens } : {}),
462
+ ...(requestIds !== undefined ? { requestIds } : {}),
463
+ ...(mc === undefined ? { mergeConfigUnavailable: true } : {})
464
+ };
465
+ }
466
+ export function attachGatewayInvokeRejectionMetadata(err, metadata) {
467
+ err.metadata = metadata;
468
+ }
318
469
  /** Default JSON string length cap for Activix `content.fullResponse` when diagnostics allow storing it. */
319
470
  export const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512_000;
320
471
  /**
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 } from './gateway-utils.js';
11
+ import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, 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';
@@ -223,6 +223,13 @@ export class AIGateway {
223
223
  failureType: 'validation-failure'
224
224
  }, startTime);
225
225
  }
226
+ const rejectMeta = buildInvokeRejectionMetadata({
227
+ request,
228
+ taskTypeId,
229
+ startTime,
230
+ gatewayAiRequestId: request.aiRequestId
231
+ });
232
+ attachGatewayInvokeRejectionMetadata(err, rejectMeta);
226
233
  // Re-throw the error so it propagates to the caller
227
234
  throw err;
228
235
  }
@@ -524,6 +531,8 @@ export class AIGateway {
524
531
  }
525
532
  const resolvedCostUsd = extractCostUsdFromRouterResponse(routerResponse);
526
533
  const routerMetaForCost = routerResponse?.metadata || {};
534
+ const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
535
+ const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
527
536
  const enhancedResponse = {
528
537
  content: content,
529
538
  parsedContent: parsedContent,
@@ -536,6 +545,8 @@ export class AIGateway {
536
545
  agentType: 'ai',
537
546
  contentType,
538
547
  parsingMethod,
548
+ ...routingMetadataSlice,
549
+ ...(effectiveModelConfig !== undefined ? { effectiveModelConfig } : {}),
539
550
  ...(typeof resolvedCostUsd === 'number'
540
551
  ? {
541
552
  costUsd: resolvedCostUsd,
@@ -545,27 +556,12 @@ export class AIGateway {
545
556
  }
546
557
  : {}),
547
558
  ...(traceEnabled
548
- ? (() => {
549
- const meta = routerResponse?.metadata || {};
550
- const provider = meta.provider || routerResponse?.provider || mergedConfig?.provider;
551
- const region = typeof meta.region === 'string' ? meta.region : undefined;
552
- const modelUsed = meta.modelUsed || meta.model || routerResponse?.model || mergedConfig?.model;
553
- const maxTokensRequested = typeof meta.maxTokensRequested === 'number'
554
- ? meta.maxTokensRequested
555
- : typeof mergedConfig?.maxTokens === 'number'
556
- ? mergedConfig.maxTokens
557
- : undefined;
558
- return {
559
- provider,
560
- region,
561
- modelUsed,
562
- maxTokensRequested,
563
- requestIds: traceRequestIds,
564
- retryCount: traceRetryCount,
565
- fallbackCount: traceFallbackCount,
566
- attempts: traceAttempts
567
- };
568
- })()
559
+ ? {
560
+ requestIds: traceRequestIds,
561
+ retryCount: traceRetryCount,
562
+ fallbackCount: traceFallbackCount,
563
+ attempts: traceAttempts
564
+ }
569
565
  : {})
570
566
  }
571
567
  };
@@ -622,8 +618,21 @@ export class AIGateway {
622
618
  }
623
619
  catch (error) {
624
620
  const err = error instanceof Error ? error : new Error(String(error));
621
+ const partial = tryExtractRouterLikePayloadFromErrorChain(err);
622
+ const rejectMeta = buildInvokeRejectionMetadata({
623
+ request,
624
+ taskTypeId,
625
+ startTime,
626
+ mergedConfig,
627
+ partialRouterPayload: partial,
628
+ gatewayAiRequestId: request.aiRequestId
629
+ });
630
+ attachGatewayInvokeRejectionMetadata(err, rejectMeta);
625
631
  if (err.message.includes(NO_PROVIDER_ERROR)) {
626
- throw new Error(err.message + NO_PROVIDER_HINT);
632
+ const wrapped = new Error(err.message + NO_PROVIDER_HINT);
633
+ wrapped.cause = err;
634
+ attachGatewayInvokeRejectionMetadata(wrapped, rejectMeta);
635
+ throw wrapped;
627
636
  }
628
637
  throw err;
629
638
  }
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, 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,34 @@ export type GatewayTraceAttempt = {
84
84
  */
85
85
  rawProviderPayload?: unknown;
86
86
  };
87
+ /**
88
+ * Normalized observability payload attached to thrown errors from {@link AIGateway.invoke}
89
+ * when the gateway can derive fields (merged config, partial router body on error).
90
+ * SDKs should read `(error as Error & { metadata?: GatewayInvokeRejectionMetadata }).metadata`
91
+ * (see docs/AI_GATEWAY_INVOKE_EXECUTION_METADATA.md).
92
+ */
93
+ export type GatewayInvokeRejectionMetadata = {
94
+ aiRequestId?: string;
95
+ identity?: ActivityIdentity;
96
+ taskTypeId?: string;
97
+ latencyMs?: number;
98
+ tokens?: {
99
+ prompt: number;
100
+ completion: number;
101
+ total: number;
102
+ };
103
+ provider?: string;
104
+ modelUsed?: string;
105
+ maxTokensRequested?: number;
106
+ region?: string;
107
+ effectiveModelConfig?: Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>>;
108
+ requestIds?: GatewayTraceRequestIds;
109
+ /**
110
+ * True when {@link mergeConfig} did not run (e.g. message-building threw first).
111
+ * Routing facts may only reflect request.config / modelConfig, not flex-md defaults.
112
+ */
113
+ mergeConfigUnavailable?: true;
114
+ };
87
115
  /**
88
116
  * Identity object used for activity linkage.
89
117
  * On gateway requests/responses it lives on `identity`. When activity tracking persists via Activix v5+,
@@ -876,7 +904,8 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
876
904
  */
877
905
  model?: string;
878
906
  /**
879
- * Provider used (e.g., 'openai', 'anthropic')
907
+ * Provider used (e.g., 'openai', 'anthropic').
908
+ * Populated on every successful invoke when router or merged config supplies it.
880
909
  */
881
910
  provider?: string;
882
911
  /**
@@ -884,23 +913,30 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
884
913
  */
885
914
  cost?: number;
886
915
  /**
887
- * Cost in USD (preferred, stable key for trace mode).
916
+ * Cost in USD (preferred stable key when the router exposes it).
888
917
  * When both are present, costUsd should mirror cost.
889
918
  */
890
919
  costUsd?: number;
891
920
  /**
892
921
  * Final effective max token cap applied (after merges/normalization), if known.
922
+ * Populated on every successful invoke when router or merged config supplies it.
893
923
  */
894
924
  maxTokensRequested?: number;
895
925
  /**
896
926
  * Model that actually served the response (after routing/fallback), if known.
897
927
  * This is distinct from requested model.
928
+ * Populated on every successful invoke when router or merged config supplies it.
898
929
  */
899
930
  modelUsed?: string;
900
931
  /**
901
932
  * Optional region identifier when applicable (provider-specific).
902
933
  */
903
934
  region?: string;
935
+ /**
936
+ * Sanitized merged generation profile (allowlisted fields only; no secrets).
937
+ * Reflects gateway merge order: modelConfig / request.config / defaults.
938
+ */
939
+ effectiveModelConfig?: Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>>;
904
940
  /**
905
941
  * Stable request/correlation identifiers across gateway/router/provider layers.
906
942
  * Only populated when diagnostics trace mode is enabled.
@@ -44,6 +44,13 @@ exports.mergeConfig = mergeConfig;
44
44
  exports.normalizeRouterUsageTokens = normalizeRouterUsageTokens;
45
45
  exports.extractTokenUsageFromRouterResponse = extractTokenUsageFromRouterResponse;
46
46
  exports.extractCostUsdFromRouterResponse = extractCostUsdFromRouterResponse;
47
+ exports.pickInvokeRoutingMetadataSlice = pickInvokeRoutingMetadataSlice;
48
+ exports.pickEffectiveModelConfigForMetadata = pickEffectiveModelConfigForMetadata;
49
+ exports.pickEffectiveModelConfigFromInvokeRequest = pickEffectiveModelConfigFromInvokeRequest;
50
+ exports.tryExtractRouterLikePayloadFromErrorChain = tryExtractRouterLikePayloadFromErrorChain;
51
+ exports.pickRequestIdsFromRouterLike = pickRequestIdsFromRouterLike;
52
+ exports.buildInvokeRejectionMetadata = buildInvokeRejectionMetadata;
53
+ exports.attachGatewayInvokeRejectionMetadata = attachGatewayInvokeRejectionMetadata;
47
54
  exports.capActivityFullResponsePayload = capActivityFullResponsePayload;
48
55
  const crypto = __importStar(require("crypto"));
49
56
  const gateway_instructions_js_1 = require("./gateway-instructions.cjs");
@@ -358,6 +365,157 @@ function extractCostUsdFromRouterResponse(routerResponse) {
358
365
  }
359
366
  return undefined;
360
367
  }
368
+ /**
369
+ * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
370
+ * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
371
+ */
372
+ function pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig) {
373
+ const rr = routerResponse != null && typeof routerResponse === 'object' ? routerResponse : {};
374
+ const meta = rr.metadata != null && typeof rr.metadata === 'object' ? rr.metadata : {};
375
+ const cfg = mergedConfig != null && typeof mergedConfig === 'object' ? mergedConfig : {};
376
+ const provider = meta.provider || rr.provider || cfg.provider;
377
+ const modelUsed = meta.modelUsed || meta.model || rr.model || cfg.model;
378
+ const maxTokensRequested = typeof meta.maxTokensRequested === 'number'
379
+ ? meta.maxTokensRequested
380
+ : typeof cfg.maxTokens === 'number'
381
+ ? cfg.maxTokens
382
+ : undefined;
383
+ const region = typeof meta.region === 'string' ? meta.region : undefined;
384
+ const out = {};
385
+ if (provider !== undefined && provider !== null)
386
+ out.provider = provider;
387
+ if (modelUsed !== undefined && modelUsed !== null)
388
+ out.modelUsed = modelUsed;
389
+ if (maxTokensRequested !== undefined)
390
+ out.maxTokensRequested = maxTokensRequested;
391
+ if (region !== undefined)
392
+ out.region = region;
393
+ return out;
394
+ }
395
+ /**
396
+ * Allowlisted generation profile from merged config for client introspection (no secrets, no arbitrary extras).
397
+ */
398
+ function pickEffectiveModelConfigForMetadata(mergedConfig) {
399
+ if (mergedConfig == null || typeof mergedConfig !== 'object')
400
+ return undefined;
401
+ const c = mergedConfig;
402
+ const keys = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
403
+ const out = {};
404
+ for (const k of keys) {
405
+ const v = c[k];
406
+ if (v !== undefined)
407
+ out[k] = v;
408
+ }
409
+ return Object.keys(out).length ? out : undefined;
410
+ }
411
+ const EFFECTIVE_MODEL_CONFIG_KEYS = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
412
+ /**
413
+ * Allowlisted generation fields from request only (before mergeConfig / flex-md).
414
+ * Priority matches mergeConfig: modelConfig overrides request.config per key.
415
+ */
416
+ function pickEffectiveModelConfigFromInvokeRequest(request) {
417
+ const cfg = (request.config ?? {});
418
+ const mc = (request.modelConfig ?? {});
419
+ const out = {};
420
+ for (const k of EFFECTIVE_MODEL_CONFIG_KEYS) {
421
+ const v = mc[k] ?? cfg[k];
422
+ if (v !== undefined)
423
+ out[k] = v;
424
+ }
425
+ const modelFromId = out.model === undefined && typeof mc.modelId === 'string' ? mc.modelId : undefined;
426
+ if (modelFromId !== undefined)
427
+ out.model = modelFromId;
428
+ return Object.keys(out).length ? out : undefined;
429
+ }
430
+ function isRouterLikeEnvelope(value) {
431
+ if (value == null || typeof value !== 'object')
432
+ return false;
433
+ const r = value;
434
+ return ('metadata' in r ||
435
+ 'outputText' in r ||
436
+ 'content' in r ||
437
+ 'requestId' in r ||
438
+ 'usage' in r);
439
+ }
440
+ /**
441
+ * Walk `error`, optional `error.cause`, and common adapter fields (`response`, `routerResponse`, …)
442
+ * to find a router-shaped object for token / correlation extraction.
443
+ */
444
+ function tryExtractRouterLikePayloadFromErrorChain(error, maxDepth = 8) {
445
+ const seen = new Set();
446
+ let cur = error;
447
+ for (let i = 0; i < maxDepth && cur != null; i++) {
448
+ if (typeof cur !== 'object')
449
+ break;
450
+ if (seen.has(cur))
451
+ break;
452
+ seen.add(cur);
453
+ const o = cur;
454
+ if (isRouterLikeEnvelope(cur))
455
+ return cur;
456
+ const nested = [o.response, o.routerResponse, o.lastResponse, o.body, o.data];
457
+ for (const n of nested) {
458
+ if (isRouterLikeEnvelope(n))
459
+ return n;
460
+ }
461
+ cur = o.cause;
462
+ }
463
+ return undefined;
464
+ }
465
+ function pickRequestIdsFromRouterLike(gatewayAiRequestId, routerLike) {
466
+ if (typeof gatewayAiRequestId !== 'string' || gatewayAiRequestId.length === 0) {
467
+ return undefined;
468
+ }
469
+ const out = { gatewayAiRequestId };
470
+ if (routerLike == null || typeof routerLike !== 'object') {
471
+ return out;
472
+ }
473
+ const rr = routerLike;
474
+ const meta = rr.metadata != null && typeof rr.metadata === 'object' ? rr.metadata : {};
475
+ const routerRequestId = rr.requestId ?? meta.requestId;
476
+ if (typeof routerRequestId === 'string')
477
+ out.routerRequestId = routerRequestId;
478
+ if (typeof meta.providerRequestId === 'string')
479
+ out.providerRequestId = meta.providerRequestId;
480
+ if (typeof meta.openrouterRequestId === 'string')
481
+ out.openrouterRequestId = meta.openrouterRequestId;
482
+ const nested = meta.requestIds;
483
+ if (nested != null && typeof nested === 'object') {
484
+ for (const [k, v] of Object.entries(nested)) {
485
+ if (typeof v === 'string')
486
+ out[k] = v;
487
+ }
488
+ }
489
+ return out;
490
+ }
491
+ function buildInvokeRejectionMetadata(args) {
492
+ const gid = args.gatewayAiRequestId ?? args.request.aiRequestId;
493
+ const partial = args.partialRouterPayload;
494
+ const mc = args.mergedConfig;
495
+ const routing = pickInvokeRoutingMetadataSlice(partial ?? {}, mc ?? {});
496
+ const effective = mc !== undefined
497
+ ? pickEffectiveModelConfigForMetadata(mc)
498
+ : pickEffectiveModelConfigFromInvokeRequest(args.request);
499
+ let tokens = partial !== undefined ? extractTokenUsageFromRouterResponse(partial) : undefined;
500
+ if (tokens && tokens.prompt === 0 && tokens.completion === 0 && tokens.total === 0) {
501
+ tokens = undefined;
502
+ }
503
+ const requestIds = pickRequestIdsFromRouterLike(gid, partial);
504
+ return {
505
+ aiRequestId: args.request.aiRequestId,
506
+ identity: args.request.identity,
507
+ taskTypeId: args.taskTypeId,
508
+ latencyMs: Date.now() - args.startTime,
509
+ ...routing,
510
+ ...(effective !== undefined ? { effectiveModelConfig: effective } : {}),
511
+ ...(tokens !== undefined ? { tokens } : {}),
512
+ ...(requestIds !== undefined ? { requestIds } : {}),
513
+ ...(mc === undefined ? { mergeConfigUnavailable: true } : {})
514
+ };
515
+ }
516
+ function attachGatewayInvokeRejectionMetadata(err, metadata) {
517
+ err.metadata = metadata;
518
+ }
361
519
  /** Default JSON string length cap for Activix `content.fullResponse` when diagnostics allow storing it. */
362
520
  exports.DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512_000;
363
521
  /**
@@ -2,7 +2,7 @@
2
2
  * Gateway Utilities Module
3
3
  * Handles utility functions
4
4
  */
5
- import type { ChatRequest, GatewayConfig } from './types.js';
5
+ import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, ModelConfig } from './types.js';
6
6
  import type { Logxer } from '@x12i/logxer';
7
7
  /**
8
8
  * Generates MD5 hash of a string
@@ -43,6 +43,41 @@ export declare function extractTokenUsageFromRouterResponse(routerResponse: unkn
43
43
  * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
44
44
  */
45
45
  export declare function extractCostUsdFromRouterResponse(routerResponse: unknown): number | undefined;
46
+ /**
47
+ * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
48
+ * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
49
+ */
50
+ export declare function pickInvokeRoutingMetadataSlice(routerResponse: unknown, mergedConfig: unknown): Partial<{
51
+ provider: string;
52
+ modelUsed: string;
53
+ maxTokensRequested: number;
54
+ region: string;
55
+ }>;
56
+ /**
57
+ * Allowlisted generation profile from merged config for client introspection (no secrets, no arbitrary extras).
58
+ */
59
+ export declare function pickEffectiveModelConfigForMetadata(mergedConfig: unknown): Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>> | undefined;
60
+ declare const EFFECTIVE_MODEL_CONFIG_KEYS: readonly ["model", "modelId", "provider", "temperature", "maxTokens", "topP"];
61
+ /**
62
+ * Allowlisted generation fields from request only (before mergeConfig / flex-md).
63
+ * Priority matches mergeConfig: modelConfig overrides request.config per key.
64
+ */
65
+ export declare function pickEffectiveModelConfigFromInvokeRequest(request: Pick<AIInvokeRequest, 'config' | 'modelConfig'>): Partial<Pick<ModelConfig, (typeof EFFECTIVE_MODEL_CONFIG_KEYS)[number]>> | undefined;
66
+ /**
67
+ * Walk `error`, optional `error.cause`, and common adapter fields (`response`, `routerResponse`, …)
68
+ * to find a router-shaped object for token / correlation extraction.
69
+ */
70
+ export declare function tryExtractRouterLikePayloadFromErrorChain(error: unknown, maxDepth?: number): unknown;
71
+ export declare function pickRequestIdsFromRouterLike(gatewayAiRequestId: string | undefined, routerLike: unknown): GatewayTraceRequestIds | undefined;
72
+ export declare function buildInvokeRejectionMetadata(args: {
73
+ request: Pick<AIInvokeRequest, 'aiRequestId' | 'identity' | 'config' | 'modelConfig'>;
74
+ taskTypeId: string;
75
+ startTime: number;
76
+ mergedConfig?: unknown;
77
+ partialRouterPayload?: unknown;
78
+ gatewayAiRequestId?: string;
79
+ }): GatewayInvokeRejectionMetadata;
80
+ export declare function attachGatewayInvokeRejectionMetadata(err: Error, metadata: GatewayInvokeRejectionMetadata): void;
46
81
  /** Default JSON string length cap for Activix `content.fullResponse` when diagnostics allow storing it. */
47
82
  export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
48
83
  /**
@@ -50,3 +85,4 @@ export declare const DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS = 512000;
50
85
  * Non-serializable values become a small marker object instead of throwing.
51
86
  */
52
87
  export declare function capActivityFullResponsePayload(payload: unknown, maxChars?: number): unknown;
88
+ export {};
@@ -226,6 +226,13 @@ class AIGateway {
226
226
  failureType: 'validation-failure'
227
227
  }, startTime);
228
228
  }
229
+ const rejectMeta = (0, gateway_utils_js_1.buildInvokeRejectionMetadata)({
230
+ request,
231
+ taskTypeId,
232
+ startTime,
233
+ gatewayAiRequestId: request.aiRequestId
234
+ });
235
+ (0, gateway_utils_js_1.attachGatewayInvokeRejectionMetadata)(err, rejectMeta);
229
236
  // Re-throw the error so it propagates to the caller
230
237
  throw err;
231
238
  }
@@ -527,6 +534,8 @@ class AIGateway {
527
534
  }
528
535
  const resolvedCostUsd = (0, gateway_utils_js_1.extractCostUsdFromRouterResponse)(routerResponse);
529
536
  const routerMetaForCost = routerResponse?.metadata || {};
537
+ const routingMetadataSlice = (0, gateway_utils_js_1.pickInvokeRoutingMetadataSlice)(routerResponse, mergedConfig);
538
+ const effectiveModelConfig = (0, gateway_utils_js_1.pickEffectiveModelConfigForMetadata)(mergedConfig);
530
539
  const enhancedResponse = {
531
540
  content: content,
532
541
  parsedContent: parsedContent,
@@ -539,6 +548,8 @@ class AIGateway {
539
548
  agentType: 'ai',
540
549
  contentType,
541
550
  parsingMethod,
551
+ ...routingMetadataSlice,
552
+ ...(effectiveModelConfig !== undefined ? { effectiveModelConfig } : {}),
542
553
  ...(typeof resolvedCostUsd === 'number'
543
554
  ? {
544
555
  costUsd: resolvedCostUsd,
@@ -548,27 +559,12 @@ class AIGateway {
548
559
  }
549
560
  : {}),
550
561
  ...(traceEnabled
551
- ? (() => {
552
- const meta = routerResponse?.metadata || {};
553
- const provider = meta.provider || routerResponse?.provider || mergedConfig?.provider;
554
- const region = typeof meta.region === 'string' ? meta.region : undefined;
555
- const modelUsed = meta.modelUsed || meta.model || routerResponse?.model || mergedConfig?.model;
556
- const maxTokensRequested = typeof meta.maxTokensRequested === 'number'
557
- ? meta.maxTokensRequested
558
- : typeof mergedConfig?.maxTokens === 'number'
559
- ? mergedConfig.maxTokens
560
- : undefined;
561
- return {
562
- provider,
563
- region,
564
- modelUsed,
565
- maxTokensRequested,
566
- requestIds: traceRequestIds,
567
- retryCount: traceRetryCount,
568
- fallbackCount: traceFallbackCount,
569
- attempts: traceAttempts
570
- };
571
- })()
562
+ ? {
563
+ requestIds: traceRequestIds,
564
+ retryCount: traceRetryCount,
565
+ fallbackCount: traceFallbackCount,
566
+ attempts: traceAttempts
567
+ }
572
568
  : {})
573
569
  }
574
570
  };
@@ -625,8 +621,21 @@ class AIGateway {
625
621
  }
626
622
  catch (error) {
627
623
  const err = error instanceof Error ? error : new Error(String(error));
624
+ const partial = (0, gateway_utils_js_1.tryExtractRouterLikePayloadFromErrorChain)(err);
625
+ const rejectMeta = (0, gateway_utils_js_1.buildInvokeRejectionMetadata)({
626
+ request,
627
+ taskTypeId,
628
+ startTime,
629
+ mergedConfig,
630
+ partialRouterPayload: partial,
631
+ gatewayAiRequestId: request.aiRequestId
632
+ });
633
+ (0, gateway_utils_js_1.attachGatewayInvokeRejectionMetadata)(err, rejectMeta);
628
634
  if (err.message.includes(NO_PROVIDER_ERROR)) {
629
- throw new Error(err.message + NO_PROVIDER_HINT);
635
+ const wrapped = new Error(err.message + NO_PROVIDER_HINT);
636
+ wrapped.cause = err;
637
+ (0, gateway_utils_js_1.attachGatewayInvokeRejectionMetadata)(wrapped, rejectMeta);
638
+ throw wrapped;
630
639
  }
631
640
  throw err;
632
641
  }
@@ -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, 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,34 @@ export type GatewayTraceAttempt = {
84
84
  */
85
85
  rawProviderPayload?: unknown;
86
86
  };
87
+ /**
88
+ * Normalized observability payload attached to thrown errors from {@link AIGateway.invoke}
89
+ * when the gateway can derive fields (merged config, partial router body on error).
90
+ * SDKs should read `(error as Error & { metadata?: GatewayInvokeRejectionMetadata }).metadata`
91
+ * (see docs/AI_GATEWAY_INVOKE_EXECUTION_METADATA.md).
92
+ */
93
+ export type GatewayInvokeRejectionMetadata = {
94
+ aiRequestId?: string;
95
+ identity?: ActivityIdentity;
96
+ taskTypeId?: string;
97
+ latencyMs?: number;
98
+ tokens?: {
99
+ prompt: number;
100
+ completion: number;
101
+ total: number;
102
+ };
103
+ provider?: string;
104
+ modelUsed?: string;
105
+ maxTokensRequested?: number;
106
+ region?: string;
107
+ effectiveModelConfig?: Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>>;
108
+ requestIds?: GatewayTraceRequestIds;
109
+ /**
110
+ * True when {@link mergeConfig} did not run (e.g. message-building threw first).
111
+ * Routing facts may only reflect request.config / modelConfig, not flex-md defaults.
112
+ */
113
+ mergeConfigUnavailable?: true;
114
+ };
87
115
  /**
88
116
  * Identity object used for activity linkage.
89
117
  * On gateway requests/responses it lives on `identity`. When activity tracking persists via Activix v5+,
@@ -876,7 +904,8 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
876
904
  */
877
905
  model?: string;
878
906
  /**
879
- * Provider used (e.g., 'openai', 'anthropic')
907
+ * Provider used (e.g., 'openai', 'anthropic').
908
+ * Populated on every successful invoke when router or merged config supplies it.
880
909
  */
881
910
  provider?: string;
882
911
  /**
@@ -884,23 +913,30 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
884
913
  */
885
914
  cost?: number;
886
915
  /**
887
- * Cost in USD (preferred, stable key for trace mode).
916
+ * Cost in USD (preferred stable key when the router exposes it).
888
917
  * When both are present, costUsd should mirror cost.
889
918
  */
890
919
  costUsd?: number;
891
920
  /**
892
921
  * Final effective max token cap applied (after merges/normalization), if known.
922
+ * Populated on every successful invoke when router or merged config supplies it.
893
923
  */
894
924
  maxTokensRequested?: number;
895
925
  /**
896
926
  * Model that actually served the response (after routing/fallback), if known.
897
927
  * This is distinct from requested model.
928
+ * Populated on every successful invoke when router or merged config supplies it.
898
929
  */
899
930
  modelUsed?: string;
900
931
  /**
901
932
  * Optional region identifier when applicable (provider-specific).
902
933
  */
903
934
  region?: string;
935
+ /**
936
+ * Sanitized merged generation profile (allowlisted fields only; no secrets).
937
+ * Reflects gateway merge order: modelConfig / request.config / defaults.
938
+ */
939
+ effectiveModelConfig?: Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>>;
904
940
  /**
905
941
  * Stable request/correlation identifiers across gateway/router/provider layers.
906
942
  * Only populated when diagnostics trace mode is enabled.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-gateway",
3
- "version": "9.1.0",
3
+ "version": "9.1.2",
4
4
  "description": "AI Gateway - Unified interface for LLM provider routing and management",
5
5
  "type": "module",
6
6
  "exports": {