@x12i/ai-providers-router 4.7.6 → 4.8.0
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/index.d.ts +7 -0
- 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/router/Router.js +10 -3
- package/dist/router/RouterTypes.d.ts +12 -0
- package/dist/router/RouterWrapper.js +1 -1
- 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/index.d.ts
CHANGED
|
@@ -7,3 +7,10 @@ export type { RequestInterceptor, ResponseInterceptor } from './interceptors.js'
|
|
|
7
7
|
export type { UsageTracker, AdapterLoader, ProviderInit } from './types.js';
|
|
8
8
|
export { Logger, getLogger, createLogger } from './logger.js';
|
|
9
9
|
export type { LogLevel, LoggerConfig } from './logger.js';
|
|
10
|
+
export { AIGateway } from './gateway.js';
|
|
11
|
+
export type { EnhancedLLMResponse } from './gateway.js';
|
|
12
|
+
export { applyResponseNormalization } from './normalization/applyResponseNormalization.js';
|
|
13
|
+
export { resolveCostReporting, extractCostUsdFromRouterResponse, extractCostUsdFromProviderUsage, hasNonZeroTokenUsage } from './normalization/cost.js';
|
|
14
|
+
export type { ActivityCostStatus, ResolvedCostReporting } from './normalization/cost.js';
|
|
15
|
+
export { resolveOutputContractFieldKeys, enrichParsedForOutputContract, contractSpecToFieldKeys } from './normalization/outputContract.js';
|
|
16
|
+
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
|
+
}
|
package/dist/router/Router.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { newId } from '../utils/ids.js';
|
|
2
|
+
import { applyResponseNormalization } from '../normalization/applyResponseNormalization.js';
|
|
3
|
+
import { extractCostUsdFromRouterResponse } from '../normalization/cost.js';
|
|
2
4
|
/**
|
|
3
5
|
* Main router class
|
|
4
6
|
* Orchestrates provider execution using ProviderModules and router-side adapters
|
|
@@ -194,6 +196,10 @@ export class AIRouter {
|
|
|
194
196
|
parsed.rawResponse?.id;
|
|
195
197
|
const openrouterRequestId = providerName === 'openrouter' ? providerRequestId : undefined;
|
|
196
198
|
const timing = { startedAt, endedAt, durationMs: endedAt - startedAt };
|
|
199
|
+
const resolvedCostUsd = extractCostUsdFromRouterResponse({
|
|
200
|
+
metadata: parsed.metadata,
|
|
201
|
+
rawResponse: parsed.rawResponse,
|
|
202
|
+
});
|
|
197
203
|
const attempt = {
|
|
198
204
|
ok: true,
|
|
199
205
|
routing: {
|
|
@@ -210,7 +216,7 @@ export class AIRouter {
|
|
|
210
216
|
usage,
|
|
211
217
|
maxTokensRequested,
|
|
212
218
|
modelUsed,
|
|
213
|
-
costUsd:
|
|
219
|
+
costUsd: resolvedCostUsd,
|
|
214
220
|
};
|
|
215
221
|
attempts.push(attempt);
|
|
216
222
|
const mergedMetadata = {
|
|
@@ -218,7 +224,7 @@ export class AIRouter {
|
|
|
218
224
|
provider: providerName,
|
|
219
225
|
modelUsed,
|
|
220
226
|
maxTokensRequested,
|
|
221
|
-
costUsd:
|
|
227
|
+
...(resolvedCostUsd !== undefined ? { costUsd: resolvedCostUsd, cost: resolvedCostUsd } : {}),
|
|
222
228
|
requestIds: {
|
|
223
229
|
...parsed.metadata?.requestIds,
|
|
224
230
|
routerRequestId: requestId,
|
|
@@ -229,13 +235,14 @@ export class AIRouter {
|
|
|
229
235
|
latencyMs: timing.durationMs,
|
|
230
236
|
attempts,
|
|
231
237
|
};
|
|
232
|
-
|
|
238
|
+
const baseResponse = {
|
|
233
239
|
...parsed,
|
|
234
240
|
requestId,
|
|
235
241
|
provider: parsed.provider || providerName,
|
|
236
242
|
usage,
|
|
237
243
|
metadata: mergedMetadata,
|
|
238
244
|
};
|
|
245
|
+
return applyResponseNormalization(baseResponse, input.request);
|
|
239
246
|
}
|
|
240
247
|
catch (err) {
|
|
241
248
|
const endedAt = Date.now();
|
|
@@ -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;
|
|
@@ -41,6 +43,10 @@ export type DiagnosticsMetadata = {
|
|
|
41
43
|
/** The actual model that served the response (may differ after normalization/routing). */
|
|
42
44
|
modelUsed?: string;
|
|
43
45
|
costUsd?: number;
|
|
46
|
+
/** Alias for activity consumers; mirrors `costUsd` when priced. */
|
|
47
|
+
cost?: number;
|
|
48
|
+
/** Set to `unpriced` when usage exists but no USD cost was returned by the provider. */
|
|
49
|
+
costStatus?: ActivityCostStatus;
|
|
44
50
|
maxTokensRequested?: number;
|
|
45
51
|
/** Stable correlation ids across router/provider proxies. */
|
|
46
52
|
requestIds?: Record<string, string | undefined>;
|
|
@@ -106,11 +112,17 @@ export interface AIRouterRequest {
|
|
|
106
112
|
/**
|
|
107
113
|
* Router response for sync mode
|
|
108
114
|
*/
|
|
115
|
+
export type NormalizedRouterOutput = {
|
|
116
|
+
/** Structured fields when an output contract is present (Run Analysis G6/G9). */
|
|
117
|
+
parsed?: Record<string, unknown>;
|
|
118
|
+
};
|
|
109
119
|
export interface AIResponse {
|
|
110
120
|
requestId: string;
|
|
111
121
|
provider: string;
|
|
112
122
|
rawResponse: unknown;
|
|
113
123
|
outputText?: string;
|
|
124
|
+
/** Gateway-normalized structured output (`output.parsed` on persisted activities). */
|
|
125
|
+
output?: NormalizedRouterOutput;
|
|
114
126
|
usage?: NormalizedUsage;
|
|
115
127
|
reasoning: {
|
|
116
128
|
requested: {
|
|
@@ -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
|
}
|
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 } 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.0",
|
|
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",
|