@x12i/ai-gateway 10.4.1 → 10.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +38 -1
  2. package/dist/activity-manager.js +24 -13
  3. package/dist/gateway-utils.d.ts +9 -1
  4. package/dist/gateway-utils.js +169 -15
  5. package/dist/gateway.js +22 -2
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +2 -2
  8. package/dist/instruction-errors.d.ts +11 -2
  9. package/dist/instruction-errors.js +8 -2
  10. package/dist/openrouter-runtime-adapter/create-openrouter-runtime-provider.js +1 -1
  11. package/dist/openrouter-runtime-adapter/index.d.ts +1 -1
  12. package/dist/openrouter-runtime-adapter/index.js +1 -1
  13. package/dist/openrouter-runtime-adapter/map-runtime-errors.js +18 -4
  14. package/dist/openrouter-runtime-adapter/map-runtime-response.d.ts +3 -0
  15. package/dist/openrouter-runtime-adapter/map-runtime-response.js +33 -0
  16. package/dist/openrouter-runtime-adapter/map-trace.js +5 -1
  17. package/dist/openrouter-runtime-adapter/validate-server-tools.js +1 -1
  18. package/dist/types.d.ts +36 -0
  19. package/dist-cjs/activity-manager.cjs +24 -13
  20. package/dist-cjs/gateway-utils.cjs +169 -15
  21. package/dist-cjs/gateway-utils.d.ts +9 -1
  22. package/dist-cjs/gateway.cjs +22 -2
  23. package/dist-cjs/index.cjs +2 -2
  24. package/dist-cjs/index.d.ts +3 -3
  25. package/dist-cjs/instruction-errors.cjs +8 -2
  26. package/dist-cjs/instruction-errors.d.ts +11 -2
  27. package/dist-cjs/openrouter-runtime-adapter/create-openrouter-runtime-provider.cjs +1 -1
  28. package/dist-cjs/openrouter-runtime-adapter/index.cjs +1 -1
  29. package/dist-cjs/openrouter-runtime-adapter/index.d.ts +1 -1
  30. package/dist-cjs/openrouter-runtime-adapter/map-runtime-errors.cjs +18 -4
  31. package/dist-cjs/openrouter-runtime-adapter/map-runtime-response.cjs +33 -0
  32. package/dist-cjs/openrouter-runtime-adapter/map-runtime-response.d.ts +3 -0
  33. package/dist-cjs/openrouter-runtime-adapter/map-trace.cjs +5 -1
  34. package/dist-cjs/openrouter-runtime-adapter/validate-server-tools.cjs +1 -1
  35. package/dist-cjs/types.d.ts +36 -0
  36. package/package.json +3 -3
@@ -1,5 +1,10 @@
1
1
  import { OpenRouterHttpError, RuntimeConfigError, } from '@x12i/openrouter-runtime';
2
2
  import { GatewayPolicyViolationError, GatewayValidationError, ProviderConfigError, ProviderInvokeError, } from '../instruction-errors.js';
3
+ import { buildPartialMetadataFromRuntimeResponse, buildRouterLikeEnvelopeFromRuntimeResponse, } from './map-runtime-response.js';
4
+ function attachRouterEnvelopeToError(err, response) {
5
+ const envelope = buildRouterLikeEnvelopeFromRuntimeResponse(response);
6
+ err.response = envelope;
7
+ }
3
8
  export function throwMappedRuntimeConfigError(err) {
4
9
  const code = err.code;
5
10
  if (code === 'OPENROUTER_API_KEY_MISSING') {
@@ -25,17 +30,26 @@ export function throwMappedOpenRouterHttpError(err) {
25
30
  export function throwMappedRuntimeResponseErrors(response) {
26
31
  if (response.status === 'completed')
27
32
  return;
33
+ const routerMetadata = buildPartialMetadataFromRuntimeResponse(response);
28
34
  const primary = response.errors[0];
29
35
  if (!primary) {
30
- throw new ProviderInvokeError('OPENROUTER_REQUEST_FAILED', 'OpenRouter runtime request failed');
36
+ const err = new ProviderInvokeError('OPENROUTER_REQUEST_FAILED', 'OpenRouter runtime request failed');
37
+ attachRouterEnvelopeToError(err, response);
38
+ throw err;
31
39
  }
32
40
  if (primary.source === 'policy' || response.status === 'policy_violation') {
33
- throw new GatewayPolicyViolationError(primary.code, primary.message);
41
+ const err = new GatewayPolicyViolationError(primary.code, primary.message, { routerMetadata });
42
+ attachRouterEnvelopeToError(err, response);
43
+ throw err;
34
44
  }
35
45
  if (primary.retryable) {
36
- throw new ProviderInvokeError(primary.code, primary.message, { retryable: true });
46
+ const err = new ProviderInvokeError(primary.code, primary.message, { retryable: true });
47
+ attachRouterEnvelopeToError(err, response);
48
+ throw err;
37
49
  }
38
- throw new ProviderInvokeError(primary.code, primary.message);
50
+ const err = new ProviderInvokeError(primary.code, primary.message);
51
+ attachRouterEnvelopeToError(err, response);
52
+ throw err;
39
53
  }
40
54
  export function mapRuntimeErrorToGatewayError(err) {
41
55
  if (err instanceof RuntimeConfigError) {
@@ -15,6 +15,9 @@ export type OpenRouterRuntimeRouterMetadata = {
15
15
  costUsd?: number;
16
16
  costStatus?: 'priced' | 'unpriced';
17
17
  };
18
+ export declare function buildPartialMetadataFromRuntimeResponse(response: RuntimeResponse): OpenRouterRuntimeRouterMetadata;
19
+ /** Router-shaped envelope for error-chain metadata extraction. */
20
+ export declare function buildRouterLikeEnvelopeFromRuntimeResponse(response: RuntimeResponse): Record<string, unknown>;
18
21
  export declare function extractOpenRouterRuntimeRouterMetadata(routerResponse: unknown): OpenRouterRuntimeRouterMetadata;
19
22
  export declare function parseRuntimeResponseToAIResponse(input: {
20
23
  requestId: string;
@@ -64,6 +64,39 @@ export function buildOpenRouterRuntimeMetadata(response) {
64
64
  requestIds: response.id ? [response.id] : undefined,
65
65
  };
66
66
  }
67
+ export function buildPartialMetadataFromRuntimeResponse(response) {
68
+ return {
69
+ serverTools: mapRuntimeToolUsage(response.toolUsage),
70
+ citations: mapRuntimeCitations(response.citations),
71
+ generatedImages: mapRuntimeImages(response.images),
72
+ patchProposals: mapRuntimePatches(response.patches),
73
+ openrouterRuntime: buildOpenRouterRuntimeMetadata(response),
74
+ };
75
+ }
76
+ /** Router-shaped envelope for error-chain metadata extraction. */
77
+ export function buildRouterLikeEnvelopeFromRuntimeResponse(response) {
78
+ const partial = buildPartialMetadataFromRuntimeResponse(response);
79
+ const usage = response.usage;
80
+ const promptTokens = usage?.inputTokens ?? 0;
81
+ const completionTokens = usage?.outputTokens ?? 0;
82
+ return {
83
+ requestId: response.id,
84
+ metadata: {
85
+ provider: 'openrouter',
86
+ modelUsed: response.model,
87
+ serverTools: partial.serverTools,
88
+ citations: partial.citations,
89
+ generatedImages: partial.generatedImages,
90
+ patchProposals: partial.patchProposals,
91
+ openrouterRuntime: partial.openrouterRuntime,
92
+ },
93
+ usage: {
94
+ promptTokens,
95
+ completionTokens,
96
+ totalTokens: usage?.totalTokens ?? promptTokens + completionTokens,
97
+ },
98
+ };
99
+ }
67
100
  export function extractOpenRouterRuntimeRouterMetadata(routerResponse) {
68
101
  if (routerResponse == null || typeof routerResponse !== 'object')
69
102
  return {};
@@ -1,5 +1,7 @@
1
1
  export function buildTraceAttemptOpenRouterRuntimeSlice(metadata) {
2
- if (!metadata.openrouterRuntime && !metadata.serverTools)
2
+ const hasPolicy = (metadata.openrouterRuntime?.policyViolations?.length ?? 0) > 0 ||
3
+ (metadata.openrouterRuntime?.warnings?.length ?? 0) > 0;
4
+ if (!metadata.openrouterRuntime && !metadata.serverTools && !hasPolicy)
3
5
  return undefined;
4
6
  return {
5
7
  apiMode: metadata.openrouterRuntime?.apiMode,
@@ -7,6 +9,8 @@ export function buildTraceAttemptOpenRouterRuntimeSlice(metadata) {
7
9
  citationCount: metadata.citations?.length ?? 0,
8
10
  generatedImageCount: metadata.generatedImages?.length ?? 0,
9
11
  patchProposalCount: metadata.patchProposals?.length ?? 0,
12
+ warnings: metadata.openrouterRuntime?.warnings ?? [],
13
+ policyViolations: metadata.openrouterRuntime?.policyViolations ?? [],
10
14
  };
11
15
  }
12
16
  export function enrichTraceOpenRouterRuntimeMetadata(base, metadata, traceEnabled) {
@@ -75,7 +75,7 @@ export function applyPostRoutingServerToolsPolicy(input) {
75
75
  validateApplyPatchConfig(serverTools, openrouter);
76
76
  if (hasRequiredServerTool(serverTools)) {
77
77
  if (!openRouterApiKey) {
78
- throw new ProviderConfigError('OPENROUTER_SERVER_TOOL_REQUIRES_KEY', 'Required OpenRouter server tools need OPENROUTER_API_KEY or GatewayConfig.openrouter.apiKey');
78
+ throw new ProviderConfigError('OPENROUTER_SERVER_TOOL_REQUIRES_KEY', 'Required OpenRouter server tools need OPENROUTER_API_KEY or GatewayConfig.openrouter.apiKey', { codeAliases: ['OPENROUTER_SERVER_TOOL_REQUIRES_OPENROUTER_KEY'] });
79
79
  }
80
80
  if (provider !== 'openrouter') {
81
81
  provider = 'openrouter';
package/dist/types.d.ts CHANGED
@@ -297,8 +297,18 @@ export type GatewayTraceAttempt = {
297
297
  citationCount?: number;
298
298
  generatedImageCount?: number;
299
299
  patchProposalCount?: number;
300
+ warnings?: GatewayWarning[];
301
+ policyViolations?: GatewayPolicyViolation[];
300
302
  };
301
303
  };
304
+ /** OpenRouter tool metadata attached to policy/invoke errors and rejection payloads. */
305
+ export type GatewayOpenRouterErrorMetadata = {
306
+ serverTools?: GatewayServerToolUsageMap;
307
+ citations?: GatewayCitation[];
308
+ generatedImages?: GatewayGeneratedImage[];
309
+ patchProposals?: GatewayPatchProposal[];
310
+ openrouterRuntime?: GatewayOpenRouterRuntimeMetadata;
311
+ };
302
312
  /**
303
313
  * Allowlisted merged router/generation config returned in {@link EnhancedLLMResponse.metadata}
304
314
  * when `diagnostics.mode === 'trace'`. Omits arbitrary extras and secrets.
@@ -349,11 +359,33 @@ export type GatewayInvokeRejectionMetadata = {
349
359
  * Sourced from {@link FallbackExhaustedError.attempts} on the router error chain.
350
360
  */
351
361
  fallbackAttempts?: GatewayFallbackAttempt[];
362
+ /**
363
+ * Structured per-attempt diagnostics (`@x12i/ai-providers-router` ≥ 4.10).
364
+ * Same attempts as {@link fallbackAttempts} when enriched; prefer this for UIs.
365
+ */
366
+ providerAttempts?: Array<{
367
+ provider: string;
368
+ modelId?: string;
369
+ httpStatus?: number;
370
+ message: string;
371
+ code?: string;
372
+ }>;
373
+ /** Dominant stable failure sub-code when fallback chain exhausts (router ≥ 4.10). */
374
+ providerSubCode?: string;
375
+ /** Short remediation hint from router enrichment (router ≥ 4.10). */
376
+ operatorHint?: string;
352
377
  /**
353
378
  * True when {@link mergeConfig} did not run (e.g. message-building threw first).
354
379
  * Routing facts may only reflect request.config / modelConfig, not flex-md defaults.
355
380
  */
356
381
  mergeConfigUnavailable?: true;
382
+ /** OpenRouter server-tool usage map when available on partial router response or policy error. */
383
+ serverTools?: GatewayServerToolUsageMap;
384
+ citations?: GatewayCitation[];
385
+ generatedImages?: GatewayGeneratedImage[];
386
+ patchProposals?: GatewayPatchProposal[];
387
+ openrouterRuntime?: GatewayOpenRouterRuntimeMetadata;
388
+ policyViolations?: GatewayPolicyViolation[];
357
389
  };
358
390
  /** Serializable slice of a router fallback attempt for rejection metadata. */
359
391
  export type GatewayFallbackAttempt = {
@@ -361,6 +393,8 @@ export type GatewayFallbackAttempt = {
361
393
  model?: string;
362
394
  httpStatus?: number;
363
395
  error: string;
396
+ /** Stable sub-code from `@x12i/ai-providers-router` ≥ 4.10 (`PROVIDER_AUTH_FAILED`, …). */
397
+ code?: string;
364
398
  responsePreview?: string;
365
399
  };
366
400
  /**
@@ -523,10 +557,12 @@ export interface ModelConfig {
523
557
  stop?: string[];
524
558
  /**
525
559
  * OpenRouter-only server tools. Ignored unless final provider is openrouter.
560
+ * Merge precedence at invoke: `modelConfig.serverTools` > `config.serverTools` > `GatewayConfig.defaultServerTools`.
526
561
  */
527
562
  serverTools?: GatewayServerToolsConfig;
528
563
  /**
529
564
  * OpenRouter-specific execution controls.
565
+ * Merge precedence at invoke: `modelConfig.openrouter` > `config.openrouter` > `GatewayConfig.openrouterRuntime` defaults.
530
566
  */
531
567
  openrouter?: GatewayOpenRouterConfig;
532
568
  /**
@@ -244,20 +244,23 @@ function pickActivixCompletionRoutingMetadata(response) {
244
244
  out.citationCount = citations.length;
245
245
  }
246
246
  const serverTools = m.serverTools;
247
+ if (serverTools != null && typeof serverTools === 'object') {
248
+ out.serverTools = serverTools;
249
+ }
247
250
  const generatedImages = m.generatedImages;
248
251
  const patchProposals = m.patchProposals;
252
+ if (Array.isArray(generatedImages) && generatedImages.length) {
253
+ out.generatedImageCount = generatedImages.length;
254
+ }
255
+ if (Array.isArray(patchProposals) && patchProposals.length) {
256
+ out.patchProposalCount = patchProposals.length;
257
+ }
249
258
  const openrouterRuntime = m.openrouterRuntime;
250
- if (serverTools != null ||
251
- openrouterRuntime != null ||
252
- (Array.isArray(generatedImages) && generatedImages.length) ||
253
- (Array.isArray(patchProposals) && patchProposals.length)) {
259
+ if (openrouterRuntime != null && typeof openrouterRuntime === 'object') {
260
+ const ort = openrouterRuntime;
254
261
  out.openrouterRuntime = {
255
- ...(openrouterRuntime != null && typeof openrouterRuntime === 'object'
256
- ? {
257
- apiMode: openrouterRuntime.apiMode,
258
- serverTools,
259
- }
260
- : { serverTools }),
262
+ ...(typeof ort.apiMode === 'string' ? { apiMode: ort.apiMode } : {}),
263
+ ...(Array.isArray(ort.warnings) ? { warnings: ort.warnings } : {}),
261
264
  citationCount: Array.isArray(citations) ? citations.length : 0,
262
265
  generatedImageCount: Array.isArray(generatedImages) ? generatedImages.length : 0,
263
266
  patchProposalCount: Array.isArray(patchProposals) ? patchProposals.length : 0,
@@ -948,6 +951,14 @@ export class ActivityManager {
948
951
  const outerMetadata = pickActivixCompletionRoutingMetadata(details.response);
949
952
  const outerCost = buildActivixOuterCost(outerMetadata, billingSlice, details.response);
950
953
  const recordMetadata = buildActivixRecordMetadata(details.response, billingSlice);
954
+ const outerOutput = details.response != null && typeof details.response === 'object'
955
+ ? {
956
+ ...details.response,
957
+ ...(Array.isArray(outerMetadata.citations) && outerMetadata.citations.length
958
+ ? { citations: outerMetadata.citations }
959
+ : {}),
960
+ }
961
+ : details.response;
951
962
  await this.activix.completeRecord(activity.activityId, {
952
963
  cost: details.cost,
953
964
  ...(typeof details.cost === 'number' && Number.isFinite(details.cost)
@@ -957,10 +968,10 @@ export class ActivityManager {
957
968
  response: details.response,
958
969
  ...(Object.keys(recordMetadata).length > 0 ? { metadata: recordMetadata } : {}),
959
970
  outer: {
960
- output: details.response,
971
+ output: outerOutput,
961
972
  metadata: outerMetadata,
962
- ...(outerCost ? { cost: outerCost } : {})
963
- }
973
+ ...(outerCost ? { cost: outerCost } : {}),
974
+ },
964
975
  }, { collection });
965
976
  this.logger.debug('Activix.completeRecord completed', {
966
977
  aiRequestId: activity.aiRequestId,
@@ -3,11 +3,11 @@
3
3
  * Handles utility functions
4
4
  */
5
5
  import * as crypto from 'crypto';
6
- import { FallbackExhaustedError } from '@x12i/ai-providers-router';
6
+ import { FallbackExhaustedError, enrichRunTaskError, providerAttemptsFromFallbackAttempts } from '@x12i/ai-providers-router';
7
7
  import { ModelResolutionError, ModelProfileInputRejectedError, ModelProfileUnroutableError, resolveInvokeModel, } from '@x12i/ai-tools';
8
8
  import { extractHttpStatusCode } from './gateway-retry.js';
9
9
  import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
10
- import { MaxTokensRequiredError, ModelRequiredError, } from './instruction-errors.js';
10
+ import { MaxTokensRequiredError, ModelRequiredError, GatewayPolicyViolationError, } from './instruction-errors.js';
11
11
  import { normalizeInvokeModelAtIngress } from './invoke-model-ingress.js';
12
12
  import { applyOnlineVariantMigration, applyPostRoutingServerToolsPolicy, mergeOpenRouterConfig, mergeServerToolsConfig, normalizeApplyPatchDefaults, resolveOpenRouterRuntimeDefaults, pickOpenRouterRuntimeMetadataSlice, enrichTraceOpenRouterRuntimeMetadata, } from './openrouter-runtime-adapter/index.js';
13
13
  import { DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, GATEWAY_DEFAULT_FREQUENCY_PENALTY, GATEWAY_DEFAULT_PRESENCE_PENALTY, GATEWAY_DEFAULT_TEMPERATURE, GATEWAY_DEFAULT_TOP_P } from './gateway-defaults.js';
@@ -626,7 +626,9 @@ function buildTraceAttemptPricingRecord(attempt, mergedConfig) {
626
626
  * Trace-mode summary: final token usage + resolved billing (after catalog pricing when applicable).
627
627
  */
628
628
  export function buildTraceUsageSummary(tokens, billing, maxTokensRequested, openrouterExtras) {
629
- if (!hasNonZeroTokenUsage(tokens) && !billing.costStatus) {
629
+ const hasOpenRouterMirror = openrouterExtras?.serverTools != null ||
630
+ (openrouterExtras?.citations?.length ?? 0) > 0;
631
+ if (!hasNonZeroTokenUsage(tokens) && !billing.costStatus && !hasOpenRouterMirror) {
630
632
  return undefined;
631
633
  }
632
634
  const summary = { tokens };
@@ -643,7 +645,7 @@ export function buildTraceUsageSummary(tokens, billing, maxTokensRequested, open
643
645
  if (billing.costBreakdown) {
644
646
  summary.costBreakdown = billing.costBreakdown;
645
647
  }
646
- if (openrouterExtras?.serverTools)
648
+ if (openrouterExtras?.serverTools != null)
647
649
  summary.serverTools = openrouterExtras.serverTools;
648
650
  if (openrouterExtras?.citations?.length)
649
651
  summary.citations = openrouterExtras.citations;
@@ -729,7 +731,16 @@ export function pickEffectiveModelConfigForMetadata(mergedConfig) {
729
731
  if (mergedConfig == null || typeof mergedConfig !== 'object')
730
732
  return undefined;
731
733
  const c = mergedConfig;
732
- const keys = ['model', 'modelId', 'provider', 'temperature', 'maxTokens', 'topP'];
734
+ const keys = [
735
+ 'model',
736
+ 'modelId',
737
+ 'provider',
738
+ 'temperature',
739
+ 'maxTokens',
740
+ 'topP',
741
+ 'serverTools',
742
+ 'openrouter',
743
+ ];
733
744
  const out = {};
734
745
  for (const k of keys) {
735
746
  const v = c[k];
@@ -822,10 +833,64 @@ export function tryExtractRouterLikePayloadFromErrorChain(error, maxDepth = 8) {
822
833
  if (isRouterLikeEnvelope(n))
823
834
  return n;
824
835
  }
836
+ const rejMeta = o.metadata;
837
+ if (rejMeta != null && typeof rejMeta === 'object' && isRouterLikeEnvelope({ metadata: rejMeta })) {
838
+ return { metadata: rejMeta };
839
+ }
840
+ if (cur instanceof GatewayPolicyViolationError && cur.routerMetadata) {
841
+ return { metadata: cur.routerMetadata };
842
+ }
825
843
  cur = o.cause;
826
844
  }
827
845
  return undefined;
828
846
  }
847
+ /** Extract OpenRouter tool metadata from invoke error chains (policy errors, partial router bodies). */
848
+ export function tryExtractOpenRouterMetadataFromErrorChain(error, maxDepth = 8) {
849
+ const partial = tryExtractRouterLikePayloadFromErrorChain(error, maxDepth);
850
+ if (partial !== undefined) {
851
+ const fromPartial = pickEnhancedOpenRouterMetadata(partial, false);
852
+ if (fromPartial.serverTools ||
853
+ fromPartial.openrouterRuntime ||
854
+ (fromPartial.citations?.length ?? 0) > 0 ||
855
+ (fromPartial.generatedImages?.length ?? 0) > 0 ||
856
+ (fromPartial.patchProposals?.length ?? 0) > 0) {
857
+ return fromPartial;
858
+ }
859
+ }
860
+ const seen = new Set();
861
+ let cur = error;
862
+ for (let i = 0; i < maxDepth && cur != null; i++) {
863
+ if (typeof cur !== 'object')
864
+ break;
865
+ if (seen.has(cur))
866
+ break;
867
+ seen.add(cur);
868
+ if (cur instanceof GatewayPolicyViolationError && cur.routerMetadata) {
869
+ return {
870
+ serverTools: cur.routerMetadata.serverTools,
871
+ citations: cur.routerMetadata.citations,
872
+ generatedImages: cur.routerMetadata.generatedImages,
873
+ patchProposals: cur.routerMetadata.patchProposals,
874
+ openrouterRuntime: cur.routerMetadata.openrouterRuntime,
875
+ };
876
+ }
877
+ const rejMeta = cur.metadata;
878
+ if (rejMeta &&
879
+ (rejMeta.serverTools ||
880
+ rejMeta.openrouterRuntime ||
881
+ (rejMeta.citations?.length ?? 0) > 0)) {
882
+ return {
883
+ serverTools: rejMeta.serverTools,
884
+ citations: rejMeta.citations,
885
+ generatedImages: rejMeta.generatedImages,
886
+ patchProposals: rejMeta.patchProposals,
887
+ openrouterRuntime: rejMeta.openrouterRuntime,
888
+ };
889
+ }
890
+ cur = cur.cause;
891
+ }
892
+ return {};
893
+ }
829
894
  export function pickRequestIdsFromRouterLike(gatewayAiRequestId, routerLike) {
830
895
  if (typeof gatewayAiRequestId !== 'string' || gatewayAiRequestId.length === 0) {
831
896
  return undefined;
@@ -915,10 +980,17 @@ export function mapGatewayFallbackAttemptsToRouter(attempts) {
915
980
  provider: a.provider,
916
981
  model: a.model,
917
982
  httpStatus: a.httpStatus,
983
+ ...(a.code !== undefined ? { code: a.code } : {}),
984
+ message: a.error,
918
985
  error: new Error(a.error),
919
- responsePreview: a.responsePreview
986
+ responsePreview: a.responsePreview,
920
987
  }));
921
988
  }
989
+ /** Apply router ≥ 4.10 provider failure enrichment (code, operatorHint, details.providerAttempts). */
990
+ export function enrichGatewayFallbackExhaustedError(exhausted) {
991
+ const routerAttempts = providerAttemptsFromFallbackAttempts(exhausted.attempts);
992
+ return enrichRunTaskError(exhausted, { providerAttempts: routerAttempts });
993
+ }
922
994
  /**
923
995
  * Log original input vs OpenRouter model id actually sent to the router after catalog resolution.
924
996
  */
@@ -944,20 +1016,58 @@ export function logResolvedModelRouting(logger, request, mergedConfig) {
944
1016
  }));
945
1017
  }
946
1018
  function mapRouterFallbackAttempts(attempts) {
947
- return attempts.map((attempt) => ({
948
- provider: String(attempt.provider),
949
- ...(attempt.model !== undefined ? { model: attempt.model } : {}),
950
- ...(attempt.httpStatus !== undefined ? { httpStatus: attempt.httpStatus } : {}),
951
- error: attempt.error instanceof Error ? attempt.error.message : String(attempt.error),
952
- ...(attempt.responsePreview !== undefined ? { responsePreview: attempt.responsePreview } : {})
1019
+ return attempts.map((attempt) => {
1020
+ const errorMsg = typeof attempt.message === 'string' && attempt.message.trim()
1021
+ ? attempt.message
1022
+ : attempt.error instanceof Error
1023
+ ? attempt.error.message
1024
+ : String(attempt.error);
1025
+ return {
1026
+ provider: String(attempt.provider),
1027
+ ...(attempt.model !== undefined ? { model: attempt.model } : {}),
1028
+ ...(attempt.httpStatus !== undefined ? { httpStatus: attempt.httpStatus } : {}),
1029
+ error: errorMsg,
1030
+ ...(attempt.code !== undefined ? { code: attempt.code } : {}),
1031
+ ...(attempt.responsePreview !== undefined ? { responsePreview: attempt.responsePreview } : {}),
1032
+ };
1033
+ });
1034
+ }
1035
+ function mapProviderAttemptDetailsToGateway(attempts) {
1036
+ return attempts.map((a) => ({
1037
+ provider: a.provider,
1038
+ ...(a.modelId !== undefined ? { model: a.modelId } : {}),
1039
+ ...(a.httpStatus !== undefined ? { httpStatus: a.httpStatus } : {}),
1040
+ error: a.message,
1041
+ ...(a.code !== undefined ? { code: a.code } : {}),
953
1042
  }));
954
1043
  }
1044
+ function extractEnrichedFieldsFromError(error) {
1045
+ if (error == null || typeof error !== 'object')
1046
+ return {};
1047
+ const e = error;
1048
+ return {
1049
+ ...(typeof e.code === 'string' ? { providerSubCode: e.code } : {}),
1050
+ ...(typeof e.operatorHint === 'string' ? { operatorHint: e.operatorHint } : {}),
1051
+ ...(e.details?.providerAttempts?.length
1052
+ ? { providerAttempts: e.details.providerAttempts }
1053
+ : {}),
1054
+ };
1055
+ }
955
1056
  function extractFallbackAttemptsFromError(error) {
956
- if (error instanceof FallbackExhaustedError && error.attempts.length > 0) {
957
- return mapRouterFallbackAttempts(error.attempts);
1057
+ if (error instanceof FallbackExhaustedError) {
1058
+ if (error.details?.providerAttempts?.length) {
1059
+ return mapProviderAttemptDetailsToGateway(error.details.providerAttempts);
1060
+ }
1061
+ if (error.attempts.length > 0) {
1062
+ return mapRouterFallbackAttempts(error.attempts);
1063
+ }
958
1064
  }
959
1065
  if (error != null && typeof error === 'object') {
960
1066
  const record = error;
1067
+ const details = record.details;
1068
+ if (details?.providerAttempts?.length) {
1069
+ return mapProviderAttemptDetailsToGateway(details.providerAttempts);
1070
+ }
961
1071
  if (record.name === 'FallbackExhaustedError' && Array.isArray(record.attempts) && record.attempts.length > 0) {
962
1072
  return mapRouterFallbackAttempts(record.attempts);
963
1073
  }
@@ -999,6 +1109,33 @@ export function buildInvokeRejectionMetadata(args) {
999
1109
  const fallbackAttempts = args.error !== undefined
1000
1110
  ? tryExtractFallbackAttemptsFromErrorChain(args.error)
1001
1111
  : undefined;
1112
+ const enriched = args.error !== undefined ? extractEnrichedFieldsFromError(args.error) : {};
1113
+ const providerAttempts = enriched.providerAttempts ??
1114
+ (fallbackAttempts?.length
1115
+ ? fallbackAttempts.map((a) => ({
1116
+ provider: a.provider,
1117
+ ...(a.model ? { modelId: a.model } : {}),
1118
+ ...(a.httpStatus !== undefined ? { httpStatus: a.httpStatus } : {}),
1119
+ message: a.error,
1120
+ ...(a.code ? { code: a.code } : {}),
1121
+ }))
1122
+ : undefined);
1123
+ const openrouterFromPartial = partial !== undefined ? pickEnhancedOpenRouterMetadata(partial, false) : {};
1124
+ const openrouterFromError = args.error !== undefined ? tryExtractOpenRouterMetadataFromErrorChain(args.error) : {};
1125
+ const openrouterMetadata = {
1126
+ ...openrouterFromPartial,
1127
+ ...(openrouterFromError.serverTools ? { serverTools: openrouterFromError.serverTools } : {}),
1128
+ ...(openrouterFromError.citations?.length ? { citations: openrouterFromError.citations } : {}),
1129
+ ...(openrouterFromError.generatedImages?.length
1130
+ ? { generatedImages: openrouterFromError.generatedImages }
1131
+ : {}),
1132
+ ...(openrouterFromError.patchProposals?.length
1133
+ ? { patchProposals: openrouterFromError.patchProposals }
1134
+ : {}),
1135
+ ...(openrouterFromError.openrouterRuntime
1136
+ ? { openrouterRuntime: openrouterFromError.openrouterRuntime }
1137
+ : {}),
1138
+ };
1002
1139
  return {
1003
1140
  aiRequestId: args.request.aiRequestId,
1004
1141
  identity: args.request.identity,
@@ -1009,7 +1146,24 @@ export function buildInvokeRejectionMetadata(args) {
1009
1146
  ...(tokens !== undefined ? { tokens } : {}),
1010
1147
  ...(requestIds !== undefined ? { requestIds } : {}),
1011
1148
  ...(fallbackAttempts !== undefined ? { fallbackAttempts } : {}),
1012
- ...(mc === undefined ? { mergeConfigUnavailable: true } : {})
1149
+ ...(providerAttempts?.length ? { providerAttempts } : {}),
1150
+ ...(enriched.providerSubCode ? { providerSubCode: enriched.providerSubCode } : {}),
1151
+ ...(enriched.operatorHint ? { operatorHint: enriched.operatorHint } : {}),
1152
+ ...(mc === undefined ? { mergeConfigUnavailable: true } : {}),
1153
+ ...(openrouterMetadata.serverTools ? { serverTools: openrouterMetadata.serverTools } : {}),
1154
+ ...(openrouterMetadata.citations?.length ? { citations: openrouterMetadata.citations } : {}),
1155
+ ...(openrouterMetadata.generatedImages?.length
1156
+ ? { generatedImages: openrouterMetadata.generatedImages }
1157
+ : {}),
1158
+ ...(openrouterMetadata.patchProposals?.length
1159
+ ? { patchProposals: openrouterMetadata.patchProposals }
1160
+ : {}),
1161
+ ...(openrouterMetadata.openrouterRuntime
1162
+ ? { openrouterRuntime: openrouterMetadata.openrouterRuntime }
1163
+ : {}),
1164
+ ...(openrouterMetadata.openrouterRuntime?.policyViolations?.length
1165
+ ? { policyViolations: openrouterMetadata.openrouterRuntime.policyViolations }
1166
+ : {}),
1013
1167
  };
1014
1168
  }
1015
1169
  export function attachGatewayInvokeRejectionMetadata(err, metadata) {
@@ -3,6 +3,8 @@
3
3
  * Handles utility functions
4
4
  */
5
5
  import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayFallbackAttempt, GatewayInvokeRejectionMetadata, GatewayTraceAttempt, GatewayTraceMergedConfig, GatewayTraceRequestIds, GatewayTraceUsageSummary, ModelConfig } from './types.js';
6
+ import { FallbackExhaustedError } from '@x12i/ai-providers-router';
7
+ import type { ProviderFailureCode } from '@x12i/ai-providers-router';
6
8
  import type { Logxer } from '@x12i/logxer';
7
9
  import { type AiCostResult, type AiModelsCatalogClient, type CostCalculator, type OpenRouterRoutingConfig } from '@x12i/ai-tools';
8
10
  import { pickOpenRouterRuntimeMetadataSlice, enrichTraceOpenRouterRuntimeMetadata } from './openrouter-runtime-adapter/index.js';
@@ -158,7 +160,7 @@ export declare function pickInvokeRoutingMetadataSlice(routerResponse: unknown,
158
160
  /**
159
161
  * Allowlisted generation profile from merged config for client introspection (no secrets, no arbitrary extras).
160
162
  */
161
- export declare function pickEffectiveModelConfigForMetadata(mergedConfig: unknown): Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP'>> | undefined;
163
+ export declare function pickEffectiveModelConfigForMetadata(mergedConfig: unknown): Partial<Pick<ModelConfig, 'model' | 'modelId' | 'provider' | 'temperature' | 'maxTokens' | 'topP' | 'serverTools' | 'openrouter'>> | undefined;
162
164
  /**
163
165
  * Allowlisted snapshot of merged router config for diagnostics trace responses (no arbitrary extras).
164
166
  */
@@ -174,6 +176,8 @@ export declare function pickEffectiveModelConfigFromInvokeRequest(request: Pick<
174
176
  * to find a router-shaped object for token / correlation extraction.
175
177
  */
176
178
  export declare function tryExtractRouterLikePayloadFromErrorChain(error: unknown, maxDepth?: number): unknown;
179
+ /** Extract OpenRouter tool metadata from invoke error chains (policy errors, partial router bodies). */
180
+ export declare function tryExtractOpenRouterMetadataFromErrorChain(error: unknown, maxDepth?: number): ReturnType<typeof pickEnhancedOpenRouterMetadata>;
177
181
  export declare function pickRequestIdsFromRouterLike(gatewayAiRequestId: string | undefined, routerLike: unknown): GatewayTraceRequestIds | undefined;
178
182
  type ModelResolutionCandidate = {
179
183
  provider: string;
@@ -189,9 +193,13 @@ export declare function mapGatewayFallbackAttemptsToRouter(attempts: GatewayFall
189
193
  provider: string;
190
194
  model?: string;
191
195
  httpStatus?: number;
196
+ code?: ProviderFailureCode;
197
+ message?: string;
192
198
  error: Error;
193
199
  responsePreview?: string;
194
200
  }>;
201
+ /** Apply router ≥ 4.10 provider failure enrichment (code, operatorHint, details.providerAttempts). */
202
+ export declare function enrichGatewayFallbackExhaustedError(exhausted: FallbackExhaustedError): FallbackExhaustedError;
195
203
  /**
196
204
  * Log original input vs OpenRouter model id actually sent to the router after catalog resolution.
197
205
  */
@@ -11,7 +11,7 @@ import { resolveRetryConfig } from './gateway-defaults.js';
11
11
  import { buildMessages } from './message-builder.js';
12
12
  import { extractJsonFromFlexMd } from './flex-md-loader.js';
13
13
  import { enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
14
- import { attachGatewayInvokeRejectionMetadata, buildGatewayFallbackAttemptsFromTrace, buildInvokeRejectionMetadata, capActivityFullResponsePayload, formatFallbackExhaustionMessage, logResolvedModelRouting, mapGatewayFallbackAttemptsToRouter, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, pickEnhancedOpenRouterMetadata, resolveCostCompletionWithAiTools, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
14
+ import { attachGatewayInvokeRejectionMetadata, buildGatewayFallbackAttemptsFromTrace, buildInvokeRejectionMetadata, capActivityFullResponsePayload, formatFallbackExhaustionMessage, logResolvedModelRouting, enrichGatewayFallbackExhaustedError, mapGatewayFallbackAttemptsToRouter, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, pickEnhancedOpenRouterMetadata, resolveCostCompletionWithAiTools, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, tryExtractRouterLikePayloadFromErrorChain, tryExtractOpenRouterMetadataFromErrorChain, } from './gateway-utils.js';
15
15
  import { buildTraceAttemptOpenRouterRuntimeSlice } from './openrouter-runtime-adapter/index.js';
16
16
  import { getAiToolsClient } from './ai-tools-client.js';
17
17
  import { autoRegisterProviders } from './gateway-provider-auto-register.js';
@@ -479,6 +479,10 @@ export class AIGateway {
479
479
  }
480
480
  else if (tryErr) {
481
481
  a.error = { name: tryErr.name || 'Error', message: capErrorMessage(tryErr.message || String(tryErr)) };
482
+ const errOpenRouter = tryExtractOpenRouterMetadataFromErrorChain(tryErr);
483
+ const runtimeSlice = buildTraceAttemptOpenRouterRuntimeSlice(errOpenRouter);
484
+ if (runtimeSlice)
485
+ a.openrouterRuntime = runtimeSlice;
482
486
  }
483
487
  }
484
488
  });
@@ -508,7 +512,7 @@ export class AIGateway {
508
512
  actual: 'All candidates failed or returned no response.'
509
513
  }
510
514
  });
511
- const exhausted = new FallbackExhaustedError(mapGatewayFallbackAttemptsToRouter(fallbackAttempts));
515
+ const exhausted = enrichGatewayFallbackExhaustedError(new FallbackExhaustedError(mapGatewayFallbackAttemptsToRouter(fallbackAttempts)));
512
516
  exhausted.message = formatFallbackExhaustionMessage(fallbackAttempts, deduped);
513
517
  if (lastError) {
514
518
  exhausted.cause = lastError;
@@ -745,6 +749,22 @@ export class AIGateway {
745
749
  error: err
746
750
  });
747
751
  attachGatewayInvokeRejectionMetadata(err, rejectMeta);
752
+ if (this.activityManager && activity) {
753
+ try {
754
+ await this.activityManager.logFailure(activity, err, {
755
+ endTime: Date.now(),
756
+ duration: Date.now() - startTime,
757
+ error: err.message,
758
+ response: { metadata: rejectMeta },
759
+ });
760
+ }
761
+ catch (activityError) {
762
+ this.logger.warn('Failed to track activity failure', {
763
+ aiRequestId: request.aiRequestId,
764
+ error: activityError instanceof Error ? activityError.message : String(activityError),
765
+ });
766
+ }
767
+ }
748
768
  if (err.message.includes(NO_PROVIDER_ERROR)) {
749
769
  const wrapped = new Error(err.message + NO_PROVIDER_HINT);
750
770
  wrapped.cause = err;
@@ -10,7 +10,7 @@ export { LLMProviderRouter } from '@x12i/ai-providers-router';
10
10
  // Re-export factory functions
11
11
  export { createRouter, createRouterFromConfig } from '@x12i/ai-providers-router';
12
12
  // Re-export error classes
13
- export { ProviderNotFoundError, FallbackExhaustedError } from '@x12i/ai-providers-router';
13
+ export { ProviderNotFoundError, FallbackExhaustedError, OpenRouterApiKeyMissingError, } from '@x12i/ai-providers-router';
14
14
  // Re-export all from router (includes everything it exports)
15
15
  export * from '@x12i/ai-providers-router';
16
16
  // Export enhanced gateway
@@ -19,7 +19,7 @@ export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError,
19
19
  export { normalizeInvokeModelAtIngress } from './invoke-model-ingress.js';
20
20
  export { autoRegisterProviders } from './gateway-provider-auto-register.js';
21
21
  export { registerOpenRouterRuntime, shouldUseOpenRouterRuntime, mapGatewayServerTools, } from './openrouter-runtime-adapter/index.js';
22
- export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, tryExtractFallbackAttemptsFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, buildGatewayPricingRecord, mapAiCostResultToResolvedActivityCost, catalogPricingSucceeded, extractUsageExtrasFromRouterResponse, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, pickEnhancedOpenRouterMetadata, hasNonZeroTokenUsage, MODEL_PROFILE_UNROUTABLE, ModelProfileUnroutableError, ModelProfileInputRejectedError, buildGatewayFallbackAttemptsFromTrace, formatFallbackExhaustionMessage, logResolvedModelRouting, mapGatewayFallbackAttemptsToRouter } from './gateway-utils.js';
22
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, tryExtractOpenRouterMetadataFromErrorChain, tryExtractFallbackAttemptsFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, buildGatewayPricingRecord, mapAiCostResultToResolvedActivityCost, catalogPricingSucceeded, extractUsageExtrasFromRouterResponse, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, pickEnhancedOpenRouterMetadata, hasNonZeroTokenUsage, MODEL_PROFILE_UNROUTABLE, ModelProfileUnroutableError, ModelProfileInputRejectedError, buildGatewayFallbackAttemptsFromTrace, formatFallbackExhaustionMessage, enrichGatewayFallbackExhaustedError, logResolvedModelRouting, mapGatewayFallbackAttemptsToRouter } from './gateway-utils.js';
23
23
  export { getGatewayOperationalMode, isProdGatewayMode, parseModelProviderSpec } from './gateway-mode.js';
24
24
  export { DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, GATEWAY_DEFAULT_FREQUENCY_PENALTY, GATEWAY_DEFAULT_PRESENCE_PENALTY, GATEWAY_DEFAULT_RETRY, GATEWAY_DEFAULT_TEMPERATURE, GATEWAY_DEFAULT_TOP_P, resolveRetryConfig } from './gateway-defaults.js';
25
25
  export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';