@x12i/ai-providers-router 4.7.7 → 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/README.md +3 -1
- package/dist/adapters/openrouter/OpenRouterAdapter.js +13 -0
- package/dist/errors.d.ts +9 -8
- package/dist/index.d.ts +10 -1
- package/dist/index.js +7 -0
- package/dist/normalization/applyResponseNormalization.d.ts +5 -0
- package/dist/normalization/applyResponseNormalization.js +36 -0
- package/dist/normalization/cost.d.ts +23 -0
- package/dist/normalization/cost.js +64 -0
- package/dist/normalization/markdownSections.d.ts +5 -0
- package/dist/normalization/markdownSections.js +78 -0
- package/dist/normalization/outputContract.d.ts +12 -0
- package/dist/normalization/outputContract.js +124 -0
- package/dist/providers/OpenRouterProvider.js +17 -0
- package/dist/router/Router.d.ts +7 -2
- package/dist/router/Router.js +117 -48
- package/dist/router/RouterTypes.d.ts +22 -0
- package/dist/router/RouterWrapper.js +2 -2
- 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 +2 -1
package/README.md
CHANGED
|
@@ -317,7 +317,9 @@ For downstream orchestration, `AIResponse` includes stable, provider-agnostic di
|
|
|
317
317
|
- `metadata.provider`: final provider used for the successful call (or last attempt)
|
|
318
318
|
- `metadata.modelUsed`: the actual model that served the response
|
|
319
319
|
- `metadata.maxTokensRequested`: final effective generation cap applied (if determinable)
|
|
320
|
-
- `metadata.costUsd`: normalized USD cost (
|
|
320
|
+
- `metadata.costUsd` / `metadata.cost`: normalized USD cost when the provider reports it (e.g. OpenRouter `usage.cost`)
|
|
321
|
+
- `metadata.costStatus`: `'priced'` when `costUsd` is set; `'unpriced'` when usage exists but no cost was returned
|
|
322
|
+
- `response.output.parsed`: structured fields when `outputContract` is on the request (markdown sections → camelCase keys)
|
|
321
323
|
- `metadata.requestIds`: `{ routerRequestId, providerRequestId?, openrouterRequestId? }`
|
|
322
324
|
- `metadata.timing`: `{ startedAt, endedAt, durationMs }` (provider-call timing)
|
|
323
325
|
- `metadata.latencyMs`: alias for `metadata.timing.durationMs`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { openRouterCatalog } from '../../openrouter-catalog.js';
|
|
2
|
+
import { extractCostUsdFromProviderUsage } from '../../normalization/cost.js';
|
|
2
3
|
import { getReasoningCapabilitiesFromRegistry, hasReasoningParamInCatalog } from './reasoning-capabilities.js';
|
|
3
4
|
/**
|
|
4
5
|
* Router-side adapter for OpenRouter provider
|
|
@@ -1008,8 +1009,20 @@ export class OpenRouterAdapter {
|
|
|
1008
1009
|
reasoning: fullResponse.reasoning,
|
|
1009
1010
|
// Don't include metadata in the copy to avoid circular reference
|
|
1010
1011
|
};
|
|
1012
|
+
const usageSources = [
|
|
1013
|
+
rawResponse?.usage,
|
|
1014
|
+
originalResponse?.usage,
|
|
1015
|
+
execResult.rawMeta?.originalResponse?.usage,
|
|
1016
|
+
];
|
|
1017
|
+
let costUsd;
|
|
1018
|
+
for (const src of usageSources) {
|
|
1019
|
+
costUsd = extractCostUsdFromProviderUsage(src);
|
|
1020
|
+
if (costUsd !== undefined)
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1011
1023
|
const metadataWithActivities = {
|
|
1012
1024
|
...fullResponse.metadata,
|
|
1025
|
+
...(costUsd !== undefined ? { costUsd, cost: costUsd } : {}),
|
|
1013
1026
|
// Include full response in metadata for ai-activities storage (without circular ref)
|
|
1014
1027
|
'ai-activities-response': fullResponseForActivities,
|
|
1015
1028
|
// Include request for complete audit trail
|
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,9 +1,18 @@
|
|
|
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';
|
|
9
11
|
export type { LogLevel, LoggerConfig } from './logger.js';
|
|
12
|
+
export { AIGateway } from './gateway.js';
|
|
13
|
+
export type { EnhancedLLMResponse } from './gateway.js';
|
|
14
|
+
export { applyResponseNormalization } from './normalization/applyResponseNormalization.js';
|
|
15
|
+
export { resolveCostReporting, extractCostUsdFromRouterResponse, extractCostUsdFromProviderUsage, hasNonZeroTokenUsage } from './normalization/cost.js';
|
|
16
|
+
export type { ActivityCostStatus, ResolvedCostReporting } from './normalization/cost.js';
|
|
17
|
+
export { resolveOutputContractFieldKeys, enrichParsedForOutputContract, contractSpecToFieldKeys } from './normalization/outputContract.js';
|
|
18
|
+
export { parseMarkdownSectionsFromContent } from './normalization/markdownSections.js';
|
package/dist/index.js
CHANGED
|
@@ -40,3 +40,10 @@ export { createRouter, createRouterFromConfig } from './factory.js';
|
|
|
40
40
|
export { ProviderNotFoundError, FallbackExhaustedError, ProviderNotInstalledError, ProviderTimeoutError } from './errors.js';
|
|
41
41
|
// Logger
|
|
42
42
|
export { Logger, getLogger, createLogger } from './logger.js';
|
|
43
|
+
// Gateway (thin invoke wrapper)
|
|
44
|
+
export { AIGateway } from './gateway.js';
|
|
45
|
+
// Response normalization (Run Analysis G6/G8)
|
|
46
|
+
export { applyResponseNormalization } from './normalization/applyResponseNormalization.js';
|
|
47
|
+
export { resolveCostReporting, extractCostUsdFromRouterResponse, extractCostUsdFromProviderUsage, hasNonZeroTokenUsage } from './normalization/cost.js';
|
|
48
|
+
export { resolveOutputContractFieldKeys, enrichParsedForOutputContract, contractSpecToFieldKeys } from './normalization/outputContract.js';
|
|
49
|
+
export { parseMarkdownSectionsFromContent } from './normalization/markdownSections.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AIResponse, AIRouterRequest } from '../router/RouterTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical post-parse normalization: cost reporting (G8) and output contract → parsed (G6/G9).
|
|
4
|
+
*/
|
|
5
|
+
export declare function applyResponseNormalization(response: AIResponse, requestContext?: AIRouterRequest['request']): AIResponse;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { extractCostUsdFromRouterResponse, resolveCostReporting } from './cost.js';
|
|
2
|
+
import { enrichParsedForOutputContract, resolveOutputContractFieldKeys } from './outputContract.js';
|
|
3
|
+
/**
|
|
4
|
+
* Canonical post-parse normalization: cost reporting (G8) and output contract → parsed (G6/G9).
|
|
5
|
+
*/
|
|
6
|
+
export function applyResponseNormalization(response, requestContext) {
|
|
7
|
+
const costUsd = extractCostUsdFromRouterResponse({
|
|
8
|
+
metadata: response.metadata,
|
|
9
|
+
rawResponse: response.rawResponse,
|
|
10
|
+
costUsd: response.metadata?.costUsd,
|
|
11
|
+
cost: response.metadata?.cost
|
|
12
|
+
});
|
|
13
|
+
const costReporting = resolveCostReporting(response.usage, costUsd);
|
|
14
|
+
const contractKeys = resolveOutputContractFieldKeys(requestContext);
|
|
15
|
+
const rawText = response.outputText ?? '';
|
|
16
|
+
const existingParsed = response.output?.parsed ??
|
|
17
|
+
response.metadata?.parsedContent ??
|
|
18
|
+
(rawText.trim() ? { rawText } : {});
|
|
19
|
+
const parsed = enrichParsedForOutputContract(existingParsed, rawText, contractKeys);
|
|
20
|
+
const metadata = {
|
|
21
|
+
...(response.metadata || {}),
|
|
22
|
+
...(costReporting.costUsd !== undefined ? { costUsd: costReporting.costUsd, cost: costReporting.costUsd } : {}),
|
|
23
|
+
...(costReporting.costStatus ? { costStatus: costReporting.costStatus } : {})
|
|
24
|
+
};
|
|
25
|
+
if (contractKeys?.length && Object.keys(parsed).length > 0) {
|
|
26
|
+
metadata.parsedContent = parsed;
|
|
27
|
+
}
|
|
28
|
+
const output = {
|
|
29
|
+
parsed: contractKeys?.length ? parsed : (response.output?.parsed ?? parsed)
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
...response,
|
|
33
|
+
metadata,
|
|
34
|
+
output
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { NormalizedUsage } from '../router/RouterTypes.js';
|
|
2
|
+
/** Activity billing state when token usage is recorded (Run Analysis G8). */
|
|
3
|
+
export type ActivityCostStatus = 'priced' | 'unpriced';
|
|
4
|
+
export type ResolvedCostReporting = {
|
|
5
|
+
costUsd?: number;
|
|
6
|
+
costStatus?: ActivityCostStatus;
|
|
7
|
+
};
|
|
8
|
+
/** Extract USD cost from a provider usage object (e.g. OpenRouter `usage.cost`). */
|
|
9
|
+
export declare function extractCostUsdFromProviderUsage(usage: unknown): number | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Best-effort USD cost from router response: metadata, attempts, rawResponse.usage.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractCostUsdFromRouterResponse(routerResponse: {
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
rawResponse?: unknown;
|
|
16
|
+
costUsd?: unknown;
|
|
17
|
+
cost?: unknown;
|
|
18
|
+
}): number | undefined;
|
|
19
|
+
export declare function hasNonZeroTokenUsage(usage?: NormalizedUsage): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* When usage exists, never leave cost unexplained: numeric cost when priced, else `costStatus: "unpriced"`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveCostReporting(usage: NormalizedUsage | undefined, costUsd: number | undefined): ResolvedCostReporting;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function firstFiniteNumber(...vals) {
|
|
2
|
+
for (const v of vals) {
|
|
3
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
4
|
+
return v;
|
|
5
|
+
}
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Extract USD cost from a provider usage object (e.g. OpenRouter `usage.cost`). */
|
|
9
|
+
export function extractCostUsdFromProviderUsage(usage) {
|
|
10
|
+
if (usage == null || typeof usage !== 'object')
|
|
11
|
+
return undefined;
|
|
12
|
+
const u = usage;
|
|
13
|
+
return firstFiniteNumber(u.cost, u.costUsd, u.total_cost, u.totalCost);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Best-effort USD cost from router response: metadata, attempts, rawResponse.usage.
|
|
17
|
+
*/
|
|
18
|
+
export function extractCostUsdFromRouterResponse(routerResponse) {
|
|
19
|
+
const meta = routerResponse.metadata;
|
|
20
|
+
const fromMeta = firstFiniteNumber(meta?.costUsd, meta?.cost);
|
|
21
|
+
if (fromMeta !== undefined)
|
|
22
|
+
return fromMeta;
|
|
23
|
+
const fromRoot = firstFiniteNumber(routerResponse.costUsd, routerResponse.cost);
|
|
24
|
+
if (fromRoot !== undefined)
|
|
25
|
+
return fromRoot;
|
|
26
|
+
const attempts = meta?.attempts;
|
|
27
|
+
if (Array.isArray(attempts)) {
|
|
28
|
+
for (let i = attempts.length - 1; i >= 0; i--) {
|
|
29
|
+
const a = attempts[i];
|
|
30
|
+
if (a != null && typeof a === 'object') {
|
|
31
|
+
const o = a;
|
|
32
|
+
const c = firstFiniteNumber(o.costUsd, o.cost);
|
|
33
|
+
if (c !== undefined)
|
|
34
|
+
return c;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const raw = routerResponse.rawResponse;
|
|
39
|
+
if (raw != null && typeof raw === 'object') {
|
|
40
|
+
const rawObj = raw;
|
|
41
|
+
const fromUsage = extractCostUsdFromProviderUsage(rawObj.usage);
|
|
42
|
+
if (fromUsage !== undefined)
|
|
43
|
+
return fromUsage;
|
|
44
|
+
return firstFiniteNumber(rawObj.cost, rawObj.costUsd);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
export function hasNonZeroTokenUsage(usage) {
|
|
49
|
+
if (!usage)
|
|
50
|
+
return false;
|
|
51
|
+
return !!(usage.promptTokens || usage.completionTokens || usage.totalTokens);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* When usage exists, never leave cost unexplained: numeric cost when priced, else `costStatus: "unpriced"`.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveCostReporting(usage, costUsd) {
|
|
57
|
+
if (typeof costUsd === 'number' && Number.isFinite(costUsd)) {
|
|
58
|
+
return { costUsd, costStatus: 'priced' };
|
|
59
|
+
}
|
|
60
|
+
if (hasNonZeroTokenUsage(usage)) {
|
|
61
|
+
return { costStatus: 'unpriced' };
|
|
62
|
+
}
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section-based markdown → camelCase fields (e.g. `### Short Answer` → `shortAnswer`).
|
|
3
|
+
*/
|
|
4
|
+
/** Parse markdown section headers into camelCase keys; returns {} when no sections found. */
|
|
5
|
+
export declare function parseMarkdownSectionsFromContent(content: string): Record<string, unknown>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section-based markdown → camelCase fields (e.g. `### Short Answer` → `shortAnswer`).
|
|
3
|
+
*/
|
|
4
|
+
function parseMarkdownList(content) {
|
|
5
|
+
const listItems = [];
|
|
6
|
+
let currentItem = '';
|
|
7
|
+
let hasBulletPoints = false;
|
|
8
|
+
let hasNumberedItems = false;
|
|
9
|
+
for (const line of content) {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
if (trimmed.startsWith('- ')) {
|
|
12
|
+
hasBulletPoints = true;
|
|
13
|
+
if (currentItem)
|
|
14
|
+
listItems.push(currentItem.trim());
|
|
15
|
+
currentItem = trimmed.substring(2).trim();
|
|
16
|
+
}
|
|
17
|
+
else if (/^\d+\.\s/.test(trimmed)) {
|
|
18
|
+
hasNumberedItems = true;
|
|
19
|
+
if (currentItem)
|
|
20
|
+
listItems.push(currentItem.trim());
|
|
21
|
+
const match = trimmed.match(/^\d+\.\s(.+)$/);
|
|
22
|
+
currentItem = match ? match[1].trim() : trimmed;
|
|
23
|
+
}
|
|
24
|
+
else if (currentItem && trimmed) {
|
|
25
|
+
currentItem += ' ' + trimmed;
|
|
26
|
+
}
|
|
27
|
+
else if (!currentItem && trimmed) {
|
|
28
|
+
currentItem = trimmed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (currentItem)
|
|
32
|
+
listItems.push(currentItem.trim());
|
|
33
|
+
return listItems.length > 1 || hasBulletPoints || hasNumberedItems
|
|
34
|
+
? listItems
|
|
35
|
+
: content.join('\n').trim();
|
|
36
|
+
}
|
|
37
|
+
function headerToCamelCase(header) {
|
|
38
|
+
return header
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-zA-Z0-9\s]/g, '')
|
|
41
|
+
.replace(/\s+/g, ' ')
|
|
42
|
+
.trim()
|
|
43
|
+
.replace(/\s+(\w)/g, (_, letter) => letter.toUpperCase());
|
|
44
|
+
}
|
|
45
|
+
function parseMarkdownSections(content) {
|
|
46
|
+
const result = {};
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
let currentSection = '';
|
|
49
|
+
let currentContent = [];
|
|
50
|
+
const flush = () => {
|
|
51
|
+
if (currentSection && currentContent.length > 0) {
|
|
52
|
+
result[headerToCamelCase(currentSection)] = parseMarkdownList(currentContent);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const headerMatch = line.match(/^(#{1,6})\s*(.+?)\s*$/);
|
|
57
|
+
if (headerMatch) {
|
|
58
|
+
flush();
|
|
59
|
+
currentSection = headerMatch[2].trim();
|
|
60
|
+
currentContent = [];
|
|
61
|
+
}
|
|
62
|
+
else if (currentSection) {
|
|
63
|
+
currentContent.push(line);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
flush();
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/** Parse markdown section headers into camelCase keys; returns {} when no sections found. */
|
|
70
|
+
export function parseMarkdownSectionsFromContent(content) {
|
|
71
|
+
if (!content || typeof content !== 'string' || !content.trim())
|
|
72
|
+
return {};
|
|
73
|
+
const sections = parseMarkdownSections(content);
|
|
74
|
+
const keys = Object.keys(sections);
|
|
75
|
+
if (keys.length === 1 && keys[0] === 'rawText')
|
|
76
|
+
return {};
|
|
77
|
+
return sections;
|
|
78
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type OutputContractSpec = string[] | {
|
|
2
|
+
properties: Record<string, unknown>;
|
|
3
|
+
};
|
|
4
|
+
export declare function contractSpecToFieldKeys(contract: unknown): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Resolves field keys from explicit `outputContract` on the request or graph-forwarded inputs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveOutputContractFieldKeys(requestContext: unknown): string[] | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Fills missing contract keys from markdown sections. Only runs when explicit contract keys were supplied.
|
|
11
|
+
*/
|
|
12
|
+
export declare function enrichParsedForOutputContract(parsed: unknown, rawContent: string, contractKeys: string[] | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parseMarkdownSectionsFromContent } from './markdownSections.js';
|
|
2
|
+
function isPlainObject(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function hasMeaningfulContractValue(value) {
|
|
6
|
+
if (value === undefined || value === null)
|
|
7
|
+
return false;
|
|
8
|
+
if (typeof value === 'string')
|
|
9
|
+
return value.trim().length > 0;
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.length > 0;
|
|
12
|
+
if (typeof value === 'object')
|
|
13
|
+
return Object.keys(value).length > 0;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
export function contractSpecToFieldKeys(contract) {
|
|
17
|
+
if (contract == null)
|
|
18
|
+
return [];
|
|
19
|
+
if (Array.isArray(contract)) {
|
|
20
|
+
return contract.filter((k) => typeof k === 'string' && k.trim().length > 0);
|
|
21
|
+
}
|
|
22
|
+
if (!isPlainObject(contract))
|
|
23
|
+
return [];
|
|
24
|
+
const properties = contract.properties;
|
|
25
|
+
if (isPlainObject(properties)) {
|
|
26
|
+
return Object.keys(properties);
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
function readOutputContractFromWorkingMemory(workingMemory) {
|
|
31
|
+
if (!isPlainObject(workingMemory))
|
|
32
|
+
return undefined;
|
|
33
|
+
const inputs = workingMemory.inputs;
|
|
34
|
+
if (isPlainObject(inputs) && inputs.outputContract !== undefined) {
|
|
35
|
+
return inputs.outputContract;
|
|
36
|
+
}
|
|
37
|
+
const input = workingMemory.input;
|
|
38
|
+
if (typeof input === 'string') {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(input);
|
|
41
|
+
if (isPlainObject(parsed) && parsed.outputContract !== undefined) {
|
|
42
|
+
return parsed.outputContract;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (isPlainObject(input) && input.outputContract !== undefined) {
|
|
50
|
+
return input.outputContract;
|
|
51
|
+
}
|
|
52
|
+
if (workingMemory.outputContract !== undefined)
|
|
53
|
+
return workingMemory.outputContract;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolves field keys from explicit `outputContract` on the request or graph-forwarded inputs.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveOutputContractFieldKeys(requestContext) {
|
|
60
|
+
if (requestContext == null || typeof requestContext !== 'object')
|
|
61
|
+
return undefined;
|
|
62
|
+
const r = requestContext;
|
|
63
|
+
const candidates = [
|
|
64
|
+
r.outputContract,
|
|
65
|
+
isPlainObject(r.config) ? r.config.outputContract : undefined,
|
|
66
|
+
readOutputContractFromWorkingMemory(r.workingMemory)
|
|
67
|
+
];
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
const keys = contractSpecToFieldKeys(candidate);
|
|
70
|
+
if (keys.length > 0)
|
|
71
|
+
return keys;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
function asParsedRecord(parsed) {
|
|
76
|
+
if (!isPlainObject(parsed))
|
|
77
|
+
return {};
|
|
78
|
+
return { ...parsed };
|
|
79
|
+
}
|
|
80
|
+
function pickAliasValue(source, key) {
|
|
81
|
+
if (hasMeaningfulContractValue(source[key]))
|
|
82
|
+
return source[key];
|
|
83
|
+
const spaced = key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
|
|
84
|
+
const title = spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
85
|
+
const aliases = [
|
|
86
|
+
key,
|
|
87
|
+
key.toLowerCase(),
|
|
88
|
+
title,
|
|
89
|
+
title.replace(/\s+/g, ' '),
|
|
90
|
+
key.replace(/([A-Z])/g, '_$1').toLowerCase()
|
|
91
|
+
];
|
|
92
|
+
for (const alias of aliases) {
|
|
93
|
+
if (hasMeaningfulContractValue(source[alias]))
|
|
94
|
+
return source[alias];
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Fills missing contract keys from markdown sections. Only runs when explicit contract keys were supplied.
|
|
100
|
+
*/
|
|
101
|
+
export function enrichParsedForOutputContract(parsed, rawContent, contractKeys) {
|
|
102
|
+
const base = asParsedRecord(parsed);
|
|
103
|
+
if (!contractKeys?.length)
|
|
104
|
+
return base;
|
|
105
|
+
const missing = contractKeys.filter((k) => !hasMeaningfulContractValue(base[k]));
|
|
106
|
+
if (missing.length === 0)
|
|
107
|
+
return base;
|
|
108
|
+
const content = typeof rawContent === 'string' && rawContent.trim().length > 0
|
|
109
|
+
? rawContent
|
|
110
|
+
: typeof base.rawText === 'string'
|
|
111
|
+
? base.rawText
|
|
112
|
+
: '';
|
|
113
|
+
if (!content.trim())
|
|
114
|
+
return base;
|
|
115
|
+
const fromMarkdown = parseMarkdownSectionsFromContent(content);
|
|
116
|
+
const merged = { ...base };
|
|
117
|
+
for (const key of missing) {
|
|
118
|
+
const value = pickAliasValue(fromMarkdown, key);
|
|
119
|
+
if (hasMeaningfulContractValue(value)) {
|
|
120
|
+
merged[key] = value;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return merged;
|
|
124
|
+
}
|
|
@@ -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,12 +1,34 @@
|
|
|
1
1
|
import { newId } from '../utils/ids.js';
|
|
2
|
+
import { applyResponseNormalization } from '../normalization/applyResponseNormalization.js';
|
|
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';
|
|
2
7
|
/**
|
|
3
8
|
* Main router class
|
|
4
9
|
* Orchestrates provider execution using ProviderModules and router-side adapters
|
|
5
10
|
*/
|
|
6
11
|
export class AIRouter {
|
|
7
|
-
constructor(providers, adapters) {
|
|
12
|
+
constructor(providers, adapters, routerConfig = {}) {
|
|
8
13
|
this.providers = providers;
|
|
9
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);
|
|
10
32
|
}
|
|
11
33
|
/**
|
|
12
34
|
* Resolve provider name from request, checking OpenRouter mode first
|
|
@@ -91,12 +113,15 @@ export class AIRouter {
|
|
|
91
113
|
async runSync(input) {
|
|
92
114
|
const requestId = input.requestId ?? newId();
|
|
93
115
|
const primaryProviderName = this.resolveProviderName(input);
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
});
|
|
100
125
|
const maxRetries = Math.max(0, Math.min(10, Number(input.exec?.retries ?? 0) || 0));
|
|
101
126
|
const attempts = [];
|
|
102
127
|
const normalizeUsage = (usage) => {
|
|
@@ -127,61 +152,73 @@ export class AIRouter {
|
|
|
127
152
|
(typeof args.maxTokens === 'number' ? args.maxTokens : undefined);
|
|
128
153
|
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
129
154
|
};
|
|
130
|
-
const summarizeError = (err) => {
|
|
131
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
132
|
-
const name = typeof e.name === 'string' ? e.name : 'Error';
|
|
133
|
-
const message = typeof e.message === 'string' ? e.message : String(err);
|
|
134
|
-
// Size cap to avoid shipping huge provider payloads.
|
|
135
|
-
const capped = message.length > 500 ? message.slice(0, 500) : message;
|
|
136
|
-
return { name, message: capped };
|
|
137
|
-
};
|
|
138
155
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
139
|
-
const isRetryableError = (err) => {
|
|
140
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
141
|
-
const name = err instanceof Error ? err.name : '';
|
|
142
|
-
const combined = `${name} ${msg}`.toLowerCase();
|
|
143
|
-
return (combined.includes('timeout') ||
|
|
144
|
-
combined.includes('timed out') ||
|
|
145
|
-
combined.includes('rate limit') ||
|
|
146
|
-
combined.includes('429') ||
|
|
147
|
-
combined.includes('econnreset') ||
|
|
148
|
-
combined.includes('etimedout') ||
|
|
149
|
-
combined.includes('503') ||
|
|
150
|
-
combined.includes('502') ||
|
|
151
|
-
combined.includes('504') ||
|
|
152
|
-
combined.includes('temporarily unavailable'));
|
|
153
|
-
};
|
|
154
156
|
const backoffMs = (retryIndex) => {
|
|
155
157
|
const base = 250 * Math.pow(2, Math.max(0, retryIndex));
|
|
156
158
|
const jitter = Math.floor(Math.random() * 100);
|
|
157
159
|
return Math.min(5000, base + jitter);
|
|
158
160
|
};
|
|
159
161
|
let lastError = undefined;
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
}
|
|
162
195
|
const provider = this.providers.get(providerName);
|
|
163
196
|
const adapter = this.adapters.get(providerName);
|
|
164
197
|
// Check capabilities
|
|
165
198
|
if (!provider.capabilities.modes.sync) {
|
|
166
199
|
throw new Error(`Provider '${providerName}' does not support sync mode`);
|
|
167
200
|
}
|
|
168
|
-
// Build call spec (once per
|
|
201
|
+
// Build call spec (once per fallback candidate; retries reuse the same spec)
|
|
169
202
|
const spec = await adapter.buildCallSpec({
|
|
170
203
|
requestId,
|
|
171
204
|
mode: 'sync',
|
|
172
|
-
request:
|
|
205
|
+
request: candidateInput.request,
|
|
173
206
|
exec: input.exec,
|
|
174
207
|
});
|
|
175
208
|
const maxTokensRequested = extractMaxTokensRequested(spec);
|
|
176
|
-
const requestedModel = spec?.args?.model ??
|
|
209
|
+
const requestedModel = spec?.args?.model ??
|
|
210
|
+
candidate.model ??
|
|
211
|
+
candidateInput.request?.config?.model ??
|
|
212
|
+
candidateInput.request?.model;
|
|
177
213
|
for (let retryIndex = 0; retryIndex <= maxRetries; retryIndex++) {
|
|
178
214
|
const startedAt = Date.now();
|
|
215
|
+
let execResult;
|
|
179
216
|
try {
|
|
180
|
-
|
|
217
|
+
execResult = await provider.execute(spec);
|
|
181
218
|
const endedAt = Date.now();
|
|
182
219
|
const parsed = adapter.parseResponse({
|
|
183
220
|
requestId,
|
|
184
|
-
request: { ...
|
|
221
|
+
request: { ...candidateInput.request, _callSpec: spec },
|
|
185
222
|
execResult,
|
|
186
223
|
});
|
|
187
224
|
const usage = normalizeUsage(parsed.usage);
|
|
@@ -194,10 +231,14 @@ export class AIRouter {
|
|
|
194
231
|
parsed.rawResponse?.id;
|
|
195
232
|
const openrouterRequestId = providerName === 'openrouter' ? providerRequestId : undefined;
|
|
196
233
|
const timing = { startedAt, endedAt, durationMs: endedAt - startedAt };
|
|
234
|
+
const resolvedCostUsd = extractCostUsdFromRouterResponse({
|
|
235
|
+
metadata: parsed.metadata,
|
|
236
|
+
rawResponse: parsed.rawResponse,
|
|
237
|
+
});
|
|
197
238
|
const attempt = {
|
|
198
239
|
ok: true,
|
|
199
240
|
routing: {
|
|
200
|
-
provider:
|
|
241
|
+
provider: candidate.provider,
|
|
201
242
|
retryIndex,
|
|
202
243
|
fallbackIndex,
|
|
203
244
|
requestIds: {
|
|
@@ -210,7 +251,7 @@ export class AIRouter {
|
|
|
210
251
|
usage,
|
|
211
252
|
maxTokensRequested,
|
|
212
253
|
modelUsed,
|
|
213
|
-
costUsd:
|
|
254
|
+
costUsd: resolvedCostUsd,
|
|
214
255
|
};
|
|
215
256
|
attempts.push(attempt);
|
|
216
257
|
const mergedMetadata = {
|
|
@@ -218,7 +259,7 @@ export class AIRouter {
|
|
|
218
259
|
provider: providerName,
|
|
219
260
|
modelUsed,
|
|
220
261
|
maxTokensRequested,
|
|
221
|
-
costUsd:
|
|
262
|
+
...(resolvedCostUsd !== undefined ? { costUsd: resolvedCostUsd, cost: resolvedCostUsd } : {}),
|
|
222
263
|
requestIds: {
|
|
223
264
|
...parsed.metadata?.requestIds,
|
|
224
265
|
routerRequestId: requestId,
|
|
@@ -229,30 +270,49 @@ export class AIRouter {
|
|
|
229
270
|
latencyMs: timing.durationMs,
|
|
230
271
|
attempts,
|
|
231
272
|
};
|
|
232
|
-
|
|
273
|
+
const baseResponse = {
|
|
233
274
|
...parsed,
|
|
234
275
|
requestId,
|
|
235
276
|
provider: parsed.provider || providerName,
|
|
236
277
|
usage,
|
|
237
278
|
metadata: mergedMetadata,
|
|
238
279
|
};
|
|
280
|
+
return applyResponseNormalization(baseResponse, candidateInput.request);
|
|
239
281
|
}
|
|
240
282
|
catch (err) {
|
|
241
283
|
const endedAt = Date.now();
|
|
242
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 };
|
|
243
290
|
attempts.push({
|
|
244
291
|
ok: false,
|
|
245
292
|
routing: {
|
|
246
|
-
provider:
|
|
293
|
+
provider: candidate.provider,
|
|
247
294
|
retryIndex,
|
|
248
295
|
fallbackIndex,
|
|
249
296
|
requestIds: { routerRequestId: requestId },
|
|
250
297
|
},
|
|
251
|
-
timing
|
|
298
|
+
timing,
|
|
252
299
|
maxTokensRequested,
|
|
253
|
-
modelUsed
|
|
300
|
+
modelUsed,
|
|
254
301
|
error: summarizeError(err),
|
|
255
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);
|
|
256
316
|
const shouldRetry = retryIndex < maxRetries && isRetryableError(err);
|
|
257
317
|
if (shouldRetry) {
|
|
258
318
|
await sleep(backoffMs(retryIndex));
|
|
@@ -262,10 +322,19 @@ export class AIRouter {
|
|
|
262
322
|
}
|
|
263
323
|
}
|
|
264
324
|
}
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
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;
|
|
269
338
|
}
|
|
270
339
|
/**
|
|
271
340
|
* Execute a streaming request
|
|
@@ -8,6 +8,8 @@ export type NormalizedUsage = {
|
|
|
8
8
|
completionTokens: number;
|
|
9
9
|
totalTokens: number;
|
|
10
10
|
};
|
|
11
|
+
/** Activity billing state when token usage is recorded (Run Analysis G8). */
|
|
12
|
+
export type ActivityCostStatus = 'priced' | 'unpriced';
|
|
11
13
|
export type AttemptTiming = {
|
|
12
14
|
startedAt: number;
|
|
13
15
|
endedAt: number;
|
|
@@ -16,6 +18,14 @@ export type AttemptTiming = {
|
|
|
16
18
|
export type AttemptErrorSummary = {
|
|
17
19
|
name: string;
|
|
18
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;
|
|
19
29
|
};
|
|
20
30
|
export type AttemptRouting = {
|
|
21
31
|
provider: string;
|
|
@@ -41,6 +51,10 @@ export type DiagnosticsMetadata = {
|
|
|
41
51
|
/** The actual model that served the response (may differ after normalization/routing). */
|
|
42
52
|
modelUsed?: string;
|
|
43
53
|
costUsd?: number;
|
|
54
|
+
/** Alias for activity consumers; mirrors `costUsd` when priced. */
|
|
55
|
+
cost?: number;
|
|
56
|
+
/** Set to `unpriced` when usage exists but no USD cost was returned by the provider. */
|
|
57
|
+
costStatus?: ActivityCostStatus;
|
|
44
58
|
maxTokensRequested?: number;
|
|
45
59
|
/** Stable correlation ids across router/provider proxies. */
|
|
46
60
|
requestIds?: Record<string, string | undefined>;
|
|
@@ -87,6 +101,8 @@ export interface RouterConfig {
|
|
|
87
101
|
httpReferer?: string;
|
|
88
102
|
xTitle?: string;
|
|
89
103
|
};
|
|
104
|
+
/** Default provider/model fallback chain when request config does not specify one. */
|
|
105
|
+
fallbackChain?: ProviderModelRef[];
|
|
90
106
|
}
|
|
91
107
|
/**
|
|
92
108
|
* Router request - the input to the router
|
|
@@ -106,11 +122,17 @@ export interface AIRouterRequest {
|
|
|
106
122
|
/**
|
|
107
123
|
* Router response for sync mode
|
|
108
124
|
*/
|
|
125
|
+
export type NormalizedRouterOutput = {
|
|
126
|
+
/** Structured fields when an output contract is present (Run Analysis G6/G9). */
|
|
127
|
+
parsed?: Record<string, unknown>;
|
|
128
|
+
};
|
|
109
129
|
export interface AIResponse {
|
|
110
130
|
requestId: string;
|
|
111
131
|
provider: string;
|
|
112
132
|
rawResponse: unknown;
|
|
113
133
|
outputText?: string;
|
|
134
|
+
/** Gateway-normalized structured output (`output.parsed` on persisted activities). */
|
|
135
|
+
output?: NormalizedRouterOutput;
|
|
114
136
|
usage?: NormalizedUsage;
|
|
115
137
|
reasoning: {
|
|
116
138
|
requested: {
|
|
@@ -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,
|
|
@@ -241,7 +241,7 @@ export class LLMProviderRouter {
|
|
|
241
241
|
output: result.usage?.completionTokens || 0,
|
|
242
242
|
total: result.usage?.totalTokens || 0,
|
|
243
243
|
},
|
|
244
|
-
cost: result.metadata?.costUsd,
|
|
244
|
+
cost: result.metadata?.costUsd ?? result.metadata?.cost,
|
|
245
245
|
success: true,
|
|
246
246
|
});
|
|
247
247
|
}
|
|
@@ -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 } 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.
|
|
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",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"fix:imports": "node fix-import-extensions.js",
|
|
19
19
|
"prepublishOnly": "rm -rf dist && npm run build && node -e \"import('fs').then(fs => fs.accessSync('dist/index.js'))\"",
|
|
20
20
|
"test": "npm run build && node --test .tests/**/*.test.js",
|
|
21
|
+
"test:cost-output": "npm run build && node --test .tests/cost-and-output-contract.test.js",
|
|
21
22
|
"test:openai": "ts-node .tests/callOpenAI.ts",
|
|
22
23
|
"test:grok": "ts-node .tests/testOpenRouter.ts",
|
|
23
24
|
"test:catalog": "ts-node .tests/testCatalog.ts",
|