@x12i/ai-providers-router 4.8.0 → 4.8.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.
package/dist/errors.d.ts CHANGED
@@ -5,18 +5,19 @@ import type { ProviderId } from './types.js';
5
5
  export declare class ProviderNotFoundError extends Error {
6
6
  constructor(providerName: ProviderId | string);
7
7
  }
8
+ export type FallbackAttempt = {
9
+ provider: ProviderId | string;
10
+ model?: string;
11
+ httpStatus?: number;
12
+ error: Error;
13
+ responsePreview?: string;
14
+ };
8
15
  /**
9
16
  * Error thrown when all providers in the fallback chain have failed
10
17
  */
11
18
  export declare class FallbackExhaustedError extends Error {
12
- attempts: Array<{
13
- provider: ProviderId;
14
- error: Error;
15
- }>;
16
- constructor(attempts: Array<{
17
- provider: ProviderId;
18
- error: Error;
19
- }>);
19
+ attempts: FallbackAttempt[];
20
+ constructor(attempts: FallbackAttempt[]);
20
21
  }
21
22
  /**
22
23
  * Error thrown when a provider package is not installed
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { LLMProviderRouter } from './router.js';
2
- export type { RouterConfig, HealthCheckResult, ProviderId, AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem, } from './router.js';
2
+ export type { RouterConfig, HealthCheckResult, ProviderId, AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem, NormalizedRouterOutput, ProviderModelRef, } from './router.js';
3
3
  export { createRouter, createRouterFromConfig } from './factory.js';
4
4
  export type { CreateRouterConfig } from './factory.js';
5
5
  export { ProviderNotFoundError, FallbackExhaustedError, ProviderNotInstalledError, ProviderTimeoutError } from './errors.js';
6
+ export type { FallbackAttempt } from './errors.js';
7
+ export type { PartialRouterPayload } from './router/partialErrorPayload.js';
6
8
  export type { RequestInterceptor, ResponseInterceptor } from './interceptors.js';
7
9
  export type { UsageTracker, AdapterLoader, ProviderInit } from './types.js';
8
10
  export { Logger, getLogger, createLogger } from './logger.js';
@@ -4,6 +4,21 @@
4
4
  */
5
5
  import { OpenRouter } from '@openrouter/sdk';
6
6
  import { ProviderTimeoutError } from '../errors.js';
7
+ function preservePartialResponseOnError(error) {
8
+ if (error == null || typeof error !== 'object')
9
+ return;
10
+ const e = error;
11
+ if (e.rawResponse !== undefined)
12
+ return;
13
+ const response = e.response;
14
+ if (response == null || typeof response !== 'object')
15
+ return;
16
+ const resp = response;
17
+ const body = resp.data ?? resp.body ?? resp;
18
+ if (body != null && typeof body === 'object') {
19
+ e.rawResponse = body;
20
+ }
21
+ }
7
22
  /**
8
23
  * Create OpenRouter ProviderModule using @openrouter/sdk directly
9
24
  */
@@ -124,6 +139,7 @@ export function createOpenRouterProvider(config) {
124
139
  if (timedOut && typeof timeoutMs === 'number') {
125
140
  throw new ProviderTimeoutError('openrouter', timeoutMs, operation);
126
141
  }
142
+ preservePartialResponseOnError(error);
127
143
  throw error;
128
144
  }
129
145
  finally {
@@ -223,6 +239,7 @@ export function createOpenRouterProvider(config) {
223
239
  if (timedOut && typeof timeoutMs === 'number') {
224
240
  throw new ProviderTimeoutError('openrouter', timeoutMs, operation);
225
241
  }
242
+ preservePartialResponseOnError(error);
226
243
  throw error;
227
244
  }
228
245
  finally {
@@ -1,6 +1,6 @@
1
1
  import type { ProviderRegistry } from '../registry/ProviderRegistry.js';
2
2
  import type { AdapterRegistry } from '../registry/AdapterRegistry.js';
3
- import type { AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem } from './RouterTypes.js';
3
+ import type { AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem, RouterConfig } from './RouterTypes.js';
4
4
  /**
5
5
  * Main router class
6
6
  * Orchestrates provider execution using ProviderModules and router-side adapters
@@ -8,7 +8,12 @@ import type { AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBat
8
8
  export declare class AIRouter {
9
9
  private providers;
10
10
  private adapters;
11
- constructor(providers: ProviderRegistry, adapters: AdapterRegistry);
11
+ private routerConfig;
12
+ constructor(providers: ProviderRegistry, adapters: AdapterRegistry, routerConfig?: RouterConfig);
13
+ /**
14
+ * Resolve provider module name for a specific fallback candidate.
15
+ */
16
+ private resolveProviderNameForCandidate;
12
17
  /**
13
18
  * Resolve provider name from request, checking OpenRouter mode first
14
19
  */
@@ -1,14 +1,34 @@
1
1
  import { newId } from '../utils/ids.js';
2
2
  import { applyResponseNormalization } from '../normalization/applyResponseNormalization.js';
3
3
  import { extractCostUsdFromRouterResponse } from '../normalization/cost.js';
4
+ import { FallbackExhaustedError } from '../errors.js';
5
+ import { buildCandidateRequest, buildFallbackCandidates, isRetryableError, summarizeError, toError, } from './fallbackUtils.js';
6
+ import { attachPartialRouterPayload, buildPartialRouterPayload } from './partialErrorPayload.js';
4
7
  /**
5
8
  * Main router class
6
9
  * Orchestrates provider execution using ProviderModules and router-side adapters
7
10
  */
8
11
  export class AIRouter {
9
- constructor(providers, adapters) {
12
+ constructor(providers, adapters, routerConfig = {}) {
10
13
  this.providers = providers;
11
14
  this.adapters = adapters;
15
+ this.routerConfig = routerConfig;
16
+ }
17
+ /**
18
+ * Resolve provider module name for a specific fallback candidate.
19
+ */
20
+ resolveProviderNameForCandidate(input, candidateProvider) {
21
+ const candidateInput = {
22
+ ...input,
23
+ request: {
24
+ ...input.request,
25
+ config: {
26
+ ...(input.request?.config ?? {}),
27
+ provider: candidateProvider,
28
+ },
29
+ },
30
+ };
31
+ return this.resolveProviderName(candidateInput);
12
32
  }
13
33
  /**
14
34
  * Resolve provider name from request, checking OpenRouter mode first
@@ -93,12 +113,15 @@ export class AIRouter {
93
113
  async runSync(input) {
94
114
  const requestId = input.requestId ?? newId();
95
115
  const primaryProviderName = this.resolveProviderName(input);
96
- const fallbackProvidersRaw = input.request?.config?.fallbackProviders;
97
- const fallbackProviders = Array.isArray(fallbackProvidersRaw)
98
- ? fallbackProvidersRaw.filter((p) => typeof p === 'string' && p.trim().length > 0).map((p) => p.trim())
99
- : [];
100
- // Candidates are ordered: primary, then fallbacks.
101
- const providerCandidates = [primaryProviderName, ...fallbackProviders.filter((p) => p !== primaryProviderName)];
116
+ const requestConfig = input.request?.config;
117
+ const primaryModel = (typeof requestConfig?.model === 'string' ? requestConfig.model : undefined) ??
118
+ (typeof input.request?.model === 'string' ? input.request.model : undefined);
119
+ const fallbackCandidates = buildFallbackCandidates({
120
+ primaryProvider: primaryProviderName,
121
+ primaryModel,
122
+ requestConfig: requestConfig && typeof requestConfig === 'object' ? requestConfig : undefined,
123
+ routerFallbackChain: this.routerConfig.fallbackChain,
124
+ });
102
125
  const maxRetries = Math.max(0, Math.min(10, Number(input.exec?.retries ?? 0) || 0));
103
126
  const attempts = [];
104
127
  const normalizeUsage = (usage) => {
@@ -129,61 +152,73 @@ export class AIRouter {
129
152
  (typeof args.maxTokens === 'number' ? args.maxTokens : undefined);
130
153
  return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
131
154
  };
132
- const summarizeError = (err) => {
133
- const e = err instanceof Error ? err : new Error(String(err));
134
- const name = typeof e.name === 'string' ? e.name : 'Error';
135
- const message = typeof e.message === 'string' ? e.message : String(err);
136
- // Size cap to avoid shipping huge provider payloads.
137
- const capped = message.length > 500 ? message.slice(0, 500) : message;
138
- return { name, message: capped };
139
- };
140
155
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
141
- const isRetryableError = (err) => {
142
- const msg = err instanceof Error ? err.message : String(err);
143
- const name = err instanceof Error ? err.name : '';
144
- const combined = `${name} ${msg}`.toLowerCase();
145
- return (combined.includes('timeout') ||
146
- combined.includes('timed out') ||
147
- combined.includes('rate limit') ||
148
- combined.includes('429') ||
149
- combined.includes('econnreset') ||
150
- combined.includes('etimedout') ||
151
- combined.includes('503') ||
152
- combined.includes('502') ||
153
- combined.includes('504') ||
154
- combined.includes('temporarily unavailable'));
155
- };
156
156
  const backoffMs = (retryIndex) => {
157
157
  const base = 250 * Math.pow(2, Math.max(0, retryIndex));
158
158
  const jitter = Math.floor(Math.random() * 100);
159
159
  return Math.min(5000, base + jitter);
160
160
  };
161
161
  let lastError = undefined;
162
- for (let fallbackIndex = 0; fallbackIndex < providerCandidates.length; fallbackIndex++) {
163
- const providerName = providerCandidates[fallbackIndex];
162
+ const failedAttemptErrors = [];
163
+ let lastPartialPayload;
164
+ for (let fallbackIndex = 0; fallbackIndex < fallbackCandidates.length; fallbackIndex++) {
165
+ const candidate = fallbackCandidates[fallbackIndex];
166
+ const candidateInput = buildCandidateRequest(input, candidate);
167
+ const providerName = this.resolveProviderNameForCandidate(candidateInput, candidate.provider);
168
+ if (!this.providers.has(providerName)) {
169
+ const err = new Error(`Provider not registered: ${providerName}. Available: ${this.providers.list().join(', ')}`);
170
+ lastError = err;
171
+ failedAttemptErrors.push(err);
172
+ const partialPayload = buildPartialRouterPayload({
173
+ requestId,
174
+ engineProvider: candidate.provider,
175
+ providerModule: providerName,
176
+ modelUsed: candidate.model,
177
+ attempts,
178
+ providerError: err,
179
+ });
180
+ lastPartialPayload = partialPayload;
181
+ attachPartialRouterPayload(err, partialPayload);
182
+ attempts.push({
183
+ ok: false,
184
+ routing: {
185
+ provider: candidate.provider,
186
+ retryIndex: 0,
187
+ fallbackIndex,
188
+ requestIds: { routerRequestId: requestId },
189
+ },
190
+ modelUsed: candidate.model,
191
+ error: summarizeError(err),
192
+ });
193
+ continue;
194
+ }
164
195
  const provider = this.providers.get(providerName);
165
196
  const adapter = this.adapters.get(providerName);
166
197
  // Check capabilities
167
198
  if (!provider.capabilities.modes.sync) {
168
199
  throw new Error(`Provider '${providerName}' does not support sync mode`);
169
200
  }
170
- // Build call spec (once per provider candidate; retries reuse the same spec)
201
+ // Build call spec (once per fallback candidate; retries reuse the same spec)
171
202
  const spec = await adapter.buildCallSpec({
172
203
  requestId,
173
204
  mode: 'sync',
174
- request: input.request,
205
+ request: candidateInput.request,
175
206
  exec: input.exec,
176
207
  });
177
208
  const maxTokensRequested = extractMaxTokensRequested(spec);
178
- const requestedModel = spec?.args?.model ?? input.request?.config?.model ?? input.request?.model;
209
+ const requestedModel = spec?.args?.model ??
210
+ candidate.model ??
211
+ candidateInput.request?.config?.model ??
212
+ candidateInput.request?.model;
179
213
  for (let retryIndex = 0; retryIndex <= maxRetries; retryIndex++) {
180
214
  const startedAt = Date.now();
215
+ let execResult;
181
216
  try {
182
- const execResult = await provider.execute(spec);
217
+ execResult = await provider.execute(spec);
183
218
  const endedAt = Date.now();
184
219
  const parsed = adapter.parseResponse({
185
220
  requestId,
186
- request: { ...input.request, _callSpec: spec }, // Include call spec for adapter access
221
+ request: { ...candidateInput.request, _callSpec: spec },
187
222
  execResult,
188
223
  });
189
224
  const usage = normalizeUsage(parsed.usage);
@@ -203,7 +238,7 @@ export class AIRouter {
203
238
  const attempt = {
204
239
  ok: true,
205
240
  routing: {
206
- provider: providerName,
241
+ provider: candidate.provider,
207
242
  retryIndex,
208
243
  fallbackIndex,
209
244
  requestIds: {
@@ -242,24 +277,42 @@ export class AIRouter {
242
277
  usage,
243
278
  metadata: mergedMetadata,
244
279
  };
245
- return applyResponseNormalization(baseResponse, input.request);
280
+ return applyResponseNormalization(baseResponse, candidateInput.request);
246
281
  }
247
282
  catch (err) {
248
283
  const endedAt = Date.now();
249
284
  lastError = err;
285
+ failedAttemptErrors.push(err);
286
+ const modelUsed = typeof requestedModel === 'string'
287
+ ? requestedModel
288
+ : candidate.model;
289
+ const timing = { startedAt, endedAt, durationMs: endedAt - startedAt };
250
290
  attempts.push({
251
291
  ok: false,
252
292
  routing: {
253
- provider: providerName,
293
+ provider: candidate.provider,
254
294
  retryIndex,
255
295
  fallbackIndex,
256
296
  requestIds: { routerRequestId: requestId },
257
297
  },
258
- timing: { startedAt, endedAt, durationMs: endedAt - startedAt },
298
+ timing,
259
299
  maxTokensRequested,
260
- modelUsed: typeof requestedModel === 'string' ? requestedModel : undefined,
300
+ modelUsed,
261
301
  error: summarizeError(err),
262
302
  });
303
+ const partialPayload = buildPartialRouterPayload({
304
+ requestId,
305
+ engineProvider: candidate.provider,
306
+ providerModule: providerName,
307
+ modelUsed,
308
+ maxTokensRequested,
309
+ timing,
310
+ attempts,
311
+ execResult,
312
+ providerError: err,
313
+ });
314
+ lastPartialPayload = partialPayload;
315
+ attachPartialRouterPayload(err, partialPayload);
263
316
  const shouldRetry = retryIndex < maxRetries && isRetryableError(err);
264
317
  if (shouldRetry) {
265
318
  await sleep(backoffMs(retryIndex));
@@ -269,10 +322,19 @@ export class AIRouter {
269
322
  }
270
323
  }
271
324
  }
272
- // Fallback chain exhausted
273
- const error = lastError instanceof Error ? lastError : new Error(String(lastError ?? 'Unknown error'));
274
- error.attempts = attempts;
275
- throw error;
325
+ const failedTraces = attempts.filter((a) => !a.ok);
326
+ const exhaustedAttempts = failedTraces.map((trace, i) => ({
327
+ provider: trace.routing.provider,
328
+ model: trace.modelUsed,
329
+ httpStatus: trace.error?.httpStatus,
330
+ error: toError(failedAttemptErrors[i] ?? lastError),
331
+ ...(trace.error?.responsePreview !== undefined ? { responsePreview: trace.error.responsePreview } : {}),
332
+ }));
333
+ const exhaustedError = new FallbackExhaustedError(exhaustedAttempts);
334
+ if (lastPartialPayload) {
335
+ attachPartialRouterPayload(exhaustedError, lastPartialPayload);
336
+ }
337
+ throw exhaustedError;
276
338
  }
277
339
  /**
278
340
  * Execute a streaming request
@@ -18,6 +18,14 @@ export type AttemptTiming = {
18
18
  export type AttemptErrorSummary = {
19
19
  name: string;
20
20
  message: string;
21
+ httpStatus?: number;
22
+ responsePreview?: string;
23
+ };
24
+ /** Provider + model reference used in fallback chains (`engine` is a gateway alias for `provider`). */
25
+ export type ProviderModelRef = {
26
+ engine?: string;
27
+ provider?: string;
28
+ model?: string;
21
29
  };
22
30
  export type AttemptRouting = {
23
31
  provider: string;
@@ -93,6 +101,8 @@ export interface RouterConfig {
93
101
  httpReferer?: string;
94
102
  xTitle?: string;
95
103
  };
104
+ /** Default provider/model fallback chain when request config does not specify one. */
105
+ fallbackChain?: ProviderModelRef[];
96
106
  }
97
107
  /**
98
108
  * Router request - the input to the router
@@ -60,7 +60,7 @@ export class LLMProviderRouter {
60
60
  this.adapterRegistry.register(new GrokAdapter());
61
61
  this.adapterRegistry.register(new OpenRouterAdapter());
62
62
  // Create router
63
- this.router = new AIRouter(this.providerRegistry, this.adapterRegistry);
63
+ this.router = new AIRouter(this.providerRegistry, this.adapterRegistry, this.config);
64
64
  this.logger.info('Router initialized with ProviderModule architecture', {
65
65
  verbose: this.logger.verbose,
66
66
  logLevel: this.logger.level,
@@ -0,0 +1,38 @@
1
+ import type { AttemptErrorSummary } from './RouterTypes.js';
2
+ export type FallbackCandidate = {
3
+ provider: string;
4
+ model?: string;
5
+ };
6
+ export type ProviderModelRef = {
7
+ engine?: string;
8
+ provider?: string;
9
+ model?: string;
10
+ };
11
+ export declare function extractHttpStatus(err: unknown): number | undefined;
12
+ export declare function extractResponsePreview(err: unknown): string | undefined;
13
+ export declare function toError(err: unknown): Error;
14
+ export declare function summarizeError(err: unknown): AttemptErrorSummary;
15
+ export declare function isModelNotFoundError(err: unknown): boolean;
16
+ /** Transient failures — retry the same provider/model candidate. */
17
+ export declare function isRetryableError(err: unknown): boolean;
18
+ /** Failures that should advance to the next fallback candidate (not retry same candidate). */
19
+ export declare function isFallbackEligibleError(err: unknown): boolean;
20
+ export declare function buildFallbackCandidates(input: {
21
+ primaryProvider: string;
22
+ primaryModel?: string;
23
+ requestConfig?: Record<string, unknown>;
24
+ routerFallbackChain?: ProviderModelRef[];
25
+ }): FallbackCandidate[];
26
+ export declare function buildCandidateRequest(input: AIRouterRequestLike, candidate: FallbackCandidate): AIRouterRequestLike;
27
+ type AIRouterRequestLike = {
28
+ request?: {
29
+ config?: Record<string, unknown>;
30
+ model?: string;
31
+ [key: string]: unknown;
32
+ };
33
+ provider?: string;
34
+ mode?: string;
35
+ exec?: unknown;
36
+ requestId?: string;
37
+ };
38
+ export {};
@@ -0,0 +1,176 @@
1
+ const PREVIEW_CAP = 500;
2
+ export function extractHttpStatus(err) {
3
+ const e = err;
4
+ if (!e)
5
+ return undefined;
6
+ for (const key of ['status', 'statusCode', 'httpStatus']) {
7
+ const v = e[key];
8
+ if (typeof v === 'number' && Number.isFinite(v))
9
+ return v;
10
+ }
11
+ const response = e.response;
12
+ if (response && typeof response.status === 'number')
13
+ return response.status;
14
+ const msg = err instanceof Error ? err.message : String(err);
15
+ const httpMatch = msg.match(/\bHTTP\s+(\d{3})\b/i);
16
+ if (httpMatch)
17
+ return Number(httpMatch[1]);
18
+ return undefined;
19
+ }
20
+ export function extractResponsePreview(err) {
21
+ const e = err;
22
+ if (!e)
23
+ return undefined;
24
+ const raw = e.body ??
25
+ e.responseBody ??
26
+ e.response?.data ??
27
+ e.data ??
28
+ e.error;
29
+ if (raw === undefined || raw === null)
30
+ return undefined;
31
+ let text;
32
+ if (typeof raw === 'string') {
33
+ text = raw;
34
+ }
35
+ else {
36
+ try {
37
+ text = JSON.stringify(raw);
38
+ }
39
+ catch {
40
+ text = String(raw);
41
+ }
42
+ }
43
+ return text.length > PREVIEW_CAP ? text.slice(0, PREVIEW_CAP) : text;
44
+ }
45
+ export function toError(err) {
46
+ return err instanceof Error ? err : new Error(String(err));
47
+ }
48
+ export function summarizeError(err) {
49
+ const e = toError(err);
50
+ const name = typeof e.name === 'string' ? e.name : 'Error';
51
+ const message = typeof e.message === 'string' ? e.message : String(err);
52
+ const capped = message.length > PREVIEW_CAP ? message.slice(0, PREVIEW_CAP) : message;
53
+ const httpStatus = extractHttpStatus(err);
54
+ const responsePreview = extractResponsePreview(err);
55
+ return {
56
+ name,
57
+ message: capped,
58
+ ...(httpStatus !== undefined ? { httpStatus } : {}),
59
+ ...(responsePreview !== undefined ? { responsePreview } : {}),
60
+ };
61
+ }
62
+ export function isModelNotFoundError(err) {
63
+ const status = extractHttpStatus(err);
64
+ if (status === 404)
65
+ return true;
66
+ const msg = err instanceof Error ? err.message : String(err);
67
+ const name = err instanceof Error ? err.name : '';
68
+ const combined = `${name} ${msg}`.toLowerCase();
69
+ return (combined.includes('model not found') ||
70
+ combined.includes('model_not_found') ||
71
+ combined.includes('model does not exist') ||
72
+ combined.includes('unknown model') ||
73
+ combined.includes('invalid model'));
74
+ }
75
+ /** Transient failures — retry the same provider/model candidate. */
76
+ export function isRetryableError(err) {
77
+ if (isModelNotFoundError(err))
78
+ return false;
79
+ const msg = err instanceof Error ? err.message : String(err);
80
+ const name = err instanceof Error ? err.name : '';
81
+ const combined = `${name} ${msg}`.toLowerCase();
82
+ return (combined.includes('timeout') ||
83
+ combined.includes('timed out') ||
84
+ combined.includes('rate limit') ||
85
+ combined.includes('429') ||
86
+ combined.includes('econnreset') ||
87
+ combined.includes('etimedout') ||
88
+ combined.includes('503') ||
89
+ combined.includes('502') ||
90
+ combined.includes('504') ||
91
+ combined.includes('temporarily unavailable'));
92
+ }
93
+ /** Failures that should advance to the next fallback candidate (not retry same candidate). */
94
+ export function isFallbackEligibleError(err) {
95
+ if (isModelNotFoundError(err))
96
+ return true;
97
+ const status = extractHttpStatus(err);
98
+ if (status !== undefined) {
99
+ if (status === 404)
100
+ return true;
101
+ if (status >= 500)
102
+ return false;
103
+ if (status === 429)
104
+ return false;
105
+ }
106
+ return !isRetryableError(err);
107
+ }
108
+ function parseProviderModelRef(entry) {
109
+ if (!entry || typeof entry !== 'object')
110
+ return undefined;
111
+ const ref = entry;
112
+ const providerRaw = ref.engine ?? ref.provider;
113
+ if (typeof providerRaw !== 'string' || !providerRaw.trim())
114
+ return undefined;
115
+ const provider = providerRaw.trim();
116
+ const model = typeof ref.model === 'string' && ref.model.trim() ? ref.model.trim() : undefined;
117
+ return { provider, model };
118
+ }
119
+ function dedupeCandidates(candidates) {
120
+ const seen = new Set();
121
+ const out = [];
122
+ for (const c of candidates) {
123
+ const key = `${c.provider}\0${c.model ?? ''}`;
124
+ if (seen.has(key))
125
+ continue;
126
+ seen.add(key);
127
+ out.push(c);
128
+ }
129
+ return out;
130
+ }
131
+ export function buildFallbackCandidates(input) {
132
+ const { primaryProvider, primaryModel, requestConfig, routerFallbackChain } = input;
133
+ const candidates = [{ provider: primaryProvider, model: primaryModel }];
134
+ const chainRaw = (Array.isArray(requestConfig?.fallbackChain) ? requestConfig.fallbackChain : undefined) ??
135
+ (Array.isArray(requestConfig?.fallbackEngines) ? requestConfig.fallbackEngines : undefined) ??
136
+ (Array.isArray(routerFallbackChain) ? routerFallbackChain : undefined);
137
+ if (Array.isArray(chainRaw) && chainRaw.length > 0) {
138
+ for (const entry of chainRaw) {
139
+ const parsed = parseProviderModelRef(entry);
140
+ if (parsed)
141
+ candidates.push(parsed);
142
+ }
143
+ }
144
+ else {
145
+ const fallbackProvidersRaw = requestConfig?.fallbackProviders;
146
+ const fallbackProviders = Array.isArray(fallbackProvidersRaw)
147
+ ? fallbackProvidersRaw
148
+ .filter((p) => typeof p === 'string' && p.trim().length > 0)
149
+ .map((p) => p.trim())
150
+ : [];
151
+ for (const provider of fallbackProviders) {
152
+ if (provider !== primaryProvider) {
153
+ candidates.push({ provider, model: primaryModel });
154
+ }
155
+ }
156
+ }
157
+ return dedupeCandidates(candidates);
158
+ }
159
+ export function buildCandidateRequest(input, candidate) {
160
+ const baseConfig = (input.request?.config && typeof input.request.config === 'object')
161
+ ? input.request.config
162
+ : {};
163
+ const model = candidate.model ?? (typeof baseConfig.model === 'string' ? baseConfig.model : undefined);
164
+ return {
165
+ ...input,
166
+ request: {
167
+ ...input.request,
168
+ ...(model !== undefined ? { model } : {}),
169
+ config: {
170
+ ...baseConfig,
171
+ provider: candidate.provider,
172
+ ...(model !== undefined ? { model } : {}),
173
+ },
174
+ },
175
+ };
176
+ }
@@ -0,0 +1,26 @@
1
+ import type { ProviderSDKExecResult } from '@x12i/ai-provider-interface';
2
+ import type { AttemptTrace, DiagnosticsMetadata, NormalizedUsage } from './RouterTypes.js';
3
+ export type PartialRouterPayload = {
4
+ requestId: string;
5
+ provider: string;
6
+ usage?: NormalizedUsage;
7
+ metadata: DiagnosticsMetadata;
8
+ rawResponse?: unknown;
9
+ };
10
+ export declare function attachPartialRouterPayload(err: unknown, payload: PartialRouterPayload): void;
11
+ export declare function normalizeUsageFromUnknown(usage: unknown): NormalizedUsage | undefined;
12
+ export declare function buildPartialRouterPayload(args: {
13
+ requestId: string;
14
+ engineProvider: string;
15
+ providerModule?: string;
16
+ modelUsed?: string;
17
+ maxTokensRequested?: number;
18
+ timing?: {
19
+ startedAt: number;
20
+ endedAt: number;
21
+ durationMs: number;
22
+ };
23
+ attempts?: AttemptTrace[];
24
+ execResult?: ProviderSDKExecResult;
25
+ providerError?: unknown;
26
+ }): PartialRouterPayload;
@@ -0,0 +1,133 @@
1
+ export function attachPartialRouterPayload(err, payload) {
2
+ if (err == null || typeof err !== 'object')
3
+ return;
4
+ err.response = payload;
5
+ }
6
+ function firstString(...values) {
7
+ for (const v of values) {
8
+ if (typeof v === 'string' && v.trim())
9
+ return v.trim();
10
+ }
11
+ return undefined;
12
+ }
13
+ export function normalizeUsageFromUnknown(usage) {
14
+ if (!usage || typeof usage !== 'object')
15
+ return undefined;
16
+ const u = usage;
17
+ const promptTokens = (typeof u.promptTokens === 'number' ? u.promptTokens : undefined) ??
18
+ (typeof u.inputTokens === 'number' ? u.inputTokens : undefined) ??
19
+ (typeof u.prompt_tokens === 'number' ? u.prompt_tokens : undefined) ??
20
+ (typeof u.input_tokens === 'number' ? u.input_tokens : undefined) ??
21
+ (typeof u.prompt === 'number' ? u.prompt : undefined);
22
+ const completionTokens = (typeof u.completionTokens === 'number' ? u.completionTokens : undefined) ??
23
+ (typeof u.outputTokens === 'number' ? u.outputTokens : undefined) ??
24
+ (typeof u.completion_tokens === 'number' ? u.completion_tokens : undefined) ??
25
+ (typeof u.output_tokens === 'number' ? u.output_tokens : undefined) ??
26
+ (typeof u.completion === 'number' ? u.completion : undefined);
27
+ const totalTokens = (typeof u.totalTokens === 'number' ? u.totalTokens : undefined) ??
28
+ (typeof u.total_tokens === 'number' ? u.total_tokens : undefined) ??
29
+ (typeof u.total === 'number' ? u.total : undefined);
30
+ if (promptTokens === undefined && completionTokens === undefined && totalTokens === undefined) {
31
+ return undefined;
32
+ }
33
+ const p = promptTokens ?? 0;
34
+ const c = completionTokens ?? 0;
35
+ return { promptTokens: p, completionTokens: c, totalTokens: totalTokens ?? p + c };
36
+ }
37
+ function extractIdsFromRaw(raw) {
38
+ if (!raw || typeof raw !== 'object')
39
+ return {};
40
+ const r = raw;
41
+ const id = firstString(r.id, r.request_id, r.requestId);
42
+ if (!id)
43
+ return {};
44
+ return { providerRequestId: id, openrouterRequestId: id };
45
+ }
46
+ function extractRawBody(err) {
47
+ if (err == null || typeof err !== 'object')
48
+ return undefined;
49
+ const e = err;
50
+ const response = e.response;
51
+ if (response != null && typeof response === 'object') {
52
+ const resp = response;
53
+ if (resp.data !== undefined)
54
+ return resp.data;
55
+ if (resp.body !== undefined)
56
+ return resp.body;
57
+ }
58
+ for (const key of ['rawResponse', 'body', 'data', 'lastResponse', 'routerResponse']) {
59
+ if (e[key] !== undefined)
60
+ return e[key];
61
+ }
62
+ const nestedError = e.error;
63
+ if (nestedError != null && typeof nestedError === 'object') {
64
+ return extractRawBody(nestedError);
65
+ }
66
+ return undefined;
67
+ }
68
+ function extractHeaderRequestId(err) {
69
+ if (err == null || typeof err !== 'object')
70
+ return undefined;
71
+ const e = err;
72
+ const response = e.response;
73
+ if (response == null || typeof response !== 'object')
74
+ return undefined;
75
+ const headers = response.headers;
76
+ if (headers == null || typeof headers !== 'object')
77
+ return undefined;
78
+ const h = headers;
79
+ return firstString(h['x-request-id'], h['x-openrouter-request-id'], h['request-id'], h['openrouter-request-id']);
80
+ }
81
+ function usageToGatewayTokens(usage) {
82
+ return {
83
+ prompt: usage.promptTokens,
84
+ completion: usage.completionTokens,
85
+ total: usage.totalTokens,
86
+ };
87
+ }
88
+ export function buildPartialRouterPayload(args) {
89
+ let usage = args.execResult?.rawResponse
90
+ ? normalizeUsageFromUnknown(args.execResult.rawResponse?.usage)
91
+ : undefined;
92
+ let rawResponse = args.execResult?.rawResponse;
93
+ let ids = rawResponse ? extractIdsFromRaw(rawResponse) : {};
94
+ const errorBody = extractRawBody(args.providerError);
95
+ if (errorBody !== undefined) {
96
+ rawResponse = rawResponse ?? errorBody;
97
+ usage = usage ?? normalizeUsageFromUnknown(errorBody?.usage);
98
+ const bodyIds = extractIdsFromRaw(errorBody);
99
+ ids = { ...bodyIds, ...ids };
100
+ }
101
+ const headerRequestId = extractHeaderRequestId(args.providerError);
102
+ if (headerRequestId) {
103
+ ids = {
104
+ providerRequestId: ids.providerRequestId ?? headerRequestId,
105
+ openrouterRequestId: ids.openrouterRequestId ?? headerRequestId,
106
+ };
107
+ }
108
+ const providerRequestId = ids.providerRequestId;
109
+ const openrouterRequestId = args.providerModule === 'openrouter' ? providerRequestId : ids.openrouterRequestId;
110
+ const metadata = {
111
+ provider: args.providerModule ?? args.engineProvider,
112
+ ...(args.modelUsed !== undefined ? { modelUsed: args.modelUsed } : {}),
113
+ ...(args.maxTokensRequested !== undefined ? { maxTokensRequested: args.maxTokensRequested } : {}),
114
+ requestIds: {
115
+ routerRequestId: args.requestId,
116
+ ...(providerRequestId ? { providerRequestId } : {}),
117
+ ...(openrouterRequestId ? { openrouterRequestId } : {}),
118
+ },
119
+ ...(args.timing ? { timing: args.timing, latencyMs: args.timing.durationMs } : {}),
120
+ ...(args.attempts ? { attempts: args.attempts } : {}),
121
+ };
122
+ if (usage) {
123
+ metadata.usage = usage;
124
+ metadata.tokens = usageToGatewayTokens(usage);
125
+ }
126
+ return {
127
+ requestId: args.requestId,
128
+ provider: args.engineProvider,
129
+ ...(usage ? { usage } : {}),
130
+ ...(rawResponse !== undefined ? { rawResponse } : {}),
131
+ metadata,
132
+ };
133
+ }
package/dist/router.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { LLMProviderRouter } from './router/RouterWrapper.js';
2
- export type { RouterConfig, AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem, ActivityCostStatus, NormalizedRouterOutput } from './router/RouterTypes.js';
2
+ export type { RouterConfig, AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem, ActivityCostStatus, NormalizedRouterOutput, ProviderModelRef, } from './router/RouterTypes.js';
3
3
  export { ProviderRegistry } from './registry/ProviderRegistry.js';
4
4
  export { AdapterRegistry } from './registry/AdapterRegistry.js';
5
5
  export { OpenAIAdapter } from './adapters/openai/OpenAIAdapter.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-providers-router",
3
- "version": "4.8.0",
3
+ "version": "4.8.2",
4
4
  "description": "Unified router for all LLM provider implementations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",