@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 +9 -8
- package/dist/index.d.ts +3 -1
- package/dist/providers/OpenRouterProvider.js +17 -0
- package/dist/router/Router.d.ts +7 -2
- package/dist/router/Router.js +108 -46
- package/dist/router/RouterTypes.d.ts +10 -0
- package/dist/router/RouterWrapper.js +1 -1
- package/dist/router/fallbackUtils.d.ts +38 -0
- package/dist/router/fallbackUtils.js +176 -0
- package/dist/router/partialErrorPayload.d.ts +26 -0
- package/dist/router/partialErrorPayload.js +133 -0
- package/dist/router.d.ts +1 -1
- package/package.json +1 -1
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:
|
|
13
|
-
|
|
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 {
|
package/dist/router/Router.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
*/
|
package/dist/router/Router.js
CHANGED
|
@@ -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
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
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:
|
|
205
|
+
request: candidateInput.request,
|
|
175
206
|
exec: input.exec,
|
|
176
207
|
});
|
|
177
208
|
const maxTokensRequested = extractMaxTokensRequested(spec);
|
|
178
|
-
const requestedModel = spec?.args?.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
|
-
|
|
217
|
+
execResult = await provider.execute(spec);
|
|
183
218
|
const endedAt = Date.now();
|
|
184
219
|
const parsed = adapter.parseResponse({
|
|
185
220
|
requestId,
|
|
186
|
-
request: { ...
|
|
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:
|
|
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,
|
|
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:
|
|
293
|
+
provider: candidate.provider,
|
|
254
294
|
retryIndex,
|
|
255
295
|
fallbackIndex,
|
|
256
296
|
requestIds: { routerRequestId: requestId },
|
|
257
297
|
},
|
|
258
|
-
timing
|
|
298
|
+
timing,
|
|
259
299
|
maxTokensRequested,
|
|
260
|
-
modelUsed
|
|
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
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
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';
|