@x12i/ai-gateway 10.0.2 → 10.0.5
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 +40 -13
- package/dist/ai-tools-client.d.ts +15 -33
- package/dist/ai-tools-client.js +57 -107
- package/dist/gateway-utils.d.ts +30 -10
- package/dist/gateway-utils.js +168 -110
- package/dist/gateway.js +8 -18
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/openrouter-routing.d.ts +3 -12
- package/dist/openrouter-routing.js +5 -34
- package/dist/types.d.ts +12 -0
- package/dist-cjs/ai-tools-client.cjs +57 -107
- package/dist-cjs/ai-tools-client.d.ts +15 -33
- package/dist-cjs/gateway-utils.cjs +168 -110
- package/dist-cjs/gateway-utils.d.ts +30 -10
- package/dist-cjs/gateway.cjs +8 -18
- package/dist-cjs/index.cjs +3 -1
- package/dist-cjs/index.d.ts +4 -1
- package/dist-cjs/openrouter-routing.cjs +5 -34
- package/dist-cjs/openrouter-routing.d.ts +3 -12
- package/dist-cjs/types.d.ts +12 -0
- package/package.json +2 -2
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @x12i/ai-tools invoke client bootstrap for the gateway.
|
|
3
|
+
* Model resolution orchestration lives in ai-tools ≥ 2.5.0 (`resolveInvokeModel`).
|
|
3
4
|
*/
|
|
4
|
-
import {
|
|
5
|
+
import { getAiToolsInvokeClient, resetAiToolsInvokeClientForTests as resetAiToolsInvokeClientForTestsUpstream, mapResolutionToRouterConfig, buildInvokeModelResolverOptions, CostCalculator, } from '@x12i/ai-tools';
|
|
5
6
|
import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
|
|
6
7
|
import { resolvePreferOpenRouter } from './openrouter-routing.js';
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
export { resolveInvokeModel, applyOpenRouterInvokePolicy, buildInvokeModelResolverOptions, enrichModelResolutionError, mapResolutionToRouterConfig, ModelProfileUnroutableError, ModelProfileInputRejectedError, MODEL_PROFILE_UNROUTABLE, getAiToolsInvokeClient, resetAiToolsInvokeClientForTests as resetAiToolsInvokeClientForTestsUpstream, createAiToolsInvokeClient, } from '@x12i/ai-tools';
|
|
9
|
+
export { resolveOpenRouterApiKey, resolvePreferOpenRouter, readPreferOpenRouterFromEnv } from './openrouter-routing.js';
|
|
9
10
|
let bootstrapFailedLogged = false;
|
|
10
|
-
function
|
|
11
|
-
return
|
|
11
|
+
function invokeClientOptions(config) {
|
|
12
|
+
return {
|
|
13
|
+
cacheTtlMs: config.aiTools?.cacheTtlMs,
|
|
14
|
+
...(config.aiTools?.bundledOnly ? { bundledOnly: true } : {}),
|
|
15
|
+
...(config.aiTools?.costIncludeBreakdown ? { costIncludeBreakdown: true } : {}),
|
|
16
|
+
cacheKey: `${config.aiTools?.cacheTtlMs ?? ''}:${config.aiTools?.costIncludeBreakdown ?? ''}:${config.aiTools?.bundledOnly ?? ''}:${config.aiTools?.catalogLane ?? ''}`,
|
|
17
|
+
};
|
|
12
18
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const env = routingEnv ?? loadOpenRouterRoutingEnv();
|
|
18
|
-
const prefer = resolvePreferOpenRouter(config);
|
|
19
|
+
function withCatalogLaneCalculator(client, config) {
|
|
20
|
+
const lane = config.aiTools?.catalogLane;
|
|
21
|
+
if (!lane)
|
|
22
|
+
return client;
|
|
19
23
|
return {
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
...client,
|
|
25
|
+
calculator: new CostCalculator(client.catalog, {
|
|
26
|
+
...(config.aiTools?.costIncludeBreakdown ? { includeBreakdown: true } : {}),
|
|
27
|
+
resolverOptions: buildInvokeModelResolverOptions({
|
|
28
|
+
routingEnv: client.routingEnv,
|
|
29
|
+
catalogLane: lane
|
|
30
|
+
})
|
|
31
|
+
})
|
|
22
32
|
};
|
|
23
33
|
}
|
|
34
|
+
/** @deprecated Use buildInvokeModelResolverOptions */
|
|
35
|
+
export function buildModelResolverOptions(config, routingEnv) {
|
|
36
|
+
return buildInvokeModelResolverOptions({
|
|
37
|
+
routingEnv,
|
|
38
|
+
preferOpenRouter: resolvePreferOpenRouter(config),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Use mapResolutionToRouterConfig from @x12i/ai-tools
|
|
43
|
+
*/
|
|
44
|
+
export function applyModelResolution(merged, resolution, gatewayDefaultEngine, inputModel) {
|
|
45
|
+
const mapped = mapResolutionToRouterConfig(resolution, { provider: merged.provider, model: inputModel ?? merged.model ?? '' }, gatewayDefaultEngine);
|
|
46
|
+
merged.provider = mapped.provider;
|
|
47
|
+
merged.model = mapped.model;
|
|
48
|
+
}
|
|
24
49
|
/**
|
|
25
50
|
* Returns catalog + calculator, or null when disabled or bootstrap fails.
|
|
26
51
|
*/
|
|
@@ -28,102 +53,27 @@ export async function getAiToolsClient(config, logger) {
|
|
|
28
53
|
if (config.aiTools?.enabled === false) {
|
|
29
54
|
return null;
|
|
30
55
|
}
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
sharedClientPromise = null;
|
|
44
|
-
sharedConfigKey = undefined;
|
|
45
|
-
bootstrapFailedLogged = false;
|
|
46
|
-
}
|
|
47
|
-
async function bootstrapAiTools(config, logger) {
|
|
48
|
-
try {
|
|
49
|
-
const routingEnv = loadOpenRouterRoutingEnv();
|
|
50
|
-
const catalog = new AiModelsCatalogClient({
|
|
51
|
-
cacheTtlMs: config.aiTools?.cacheTtlMs,
|
|
52
|
-
...(config.aiTools?.bundledOnly ? { bundledOnly: true } : {}),
|
|
53
|
-
resolverOptions: { routingEnv },
|
|
54
|
-
});
|
|
55
|
-
const calculator = new CostCalculator(catalog, {
|
|
56
|
-
includeBreakdown: config.aiTools?.costIncludeBreakdown === true,
|
|
57
|
-
});
|
|
56
|
+
const client = await getAiToolsInvokeClient(invokeClientOptions(config), {
|
|
57
|
+
warn: (msg, err) => {
|
|
58
|
+
if (!bootstrapFailedLogged) {
|
|
59
|
+
bootstrapFailedLogged = true;
|
|
60
|
+
logger.warn(msg, withActivityIdentity(undefined, {
|
|
61
|
+
error: err instanceof Error ? err.message : String(err),
|
|
62
|
+
debugKind: gatewayLogDebug.anomaly,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (client) {
|
|
58
68
|
logger.debug('ai-tools catalog client ready', {
|
|
59
69
|
debugKind: gatewayLogDebug.state,
|
|
60
70
|
});
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
if (!bootstrapFailedLogged) {
|
|
65
|
-
bootstrapFailedLogged = true;
|
|
66
|
-
logger.warn('ai-tools catalog bootstrap failed; model resolution and catalog cost calculation disabled', withActivityIdentity(undefined, {
|
|
67
|
-
error: error instanceof Error ? error.message : String(error),
|
|
68
|
-
debugKind: gatewayLogDebug.anomaly,
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Map catalog resolution to router `{ provider, model }` (agnostic to openrouter vs vendor input).
|
|
76
|
-
*/
|
|
77
|
-
export function applyModelResolution(merged, resolution, gatewayDefaultEngine, inputModel) {
|
|
78
|
-
const ref = resolveModelVendorFromResolution(resolution, inputModel ?? merged.model ?? '', {
|
|
79
|
-
asOpenRouter: resolution.routedViaOpenRouter,
|
|
80
|
-
});
|
|
81
|
-
if (ref) {
|
|
82
|
-
merged.provider = ref.provider;
|
|
83
|
-
merged.model = ref.model;
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (resolution.routedViaOpenRouter) {
|
|
87
|
-
merged.provider = 'openrouter';
|
|
88
|
-
merged.model = resolution.modelId;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const slash = resolution.modelId.indexOf('/');
|
|
92
|
-
if (slash > 0) {
|
|
93
|
-
merged.provider = resolution.record?.providerId ?? resolution.modelId.slice(0, slash);
|
|
94
|
-
merged.model = resolution.modelId.slice(slash + 1);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
merged.model = resolution.modelId;
|
|
98
|
-
if (resolution.record?.providerId) {
|
|
99
|
-
merged.provider = resolution.record.providerId;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (!merged.provider && gatewayDefaultEngine) {
|
|
103
|
-
merged.provider = gatewayDefaultEngine;
|
|
71
|
+
return withCatalogLaneCalculator(client, config);
|
|
104
72
|
}
|
|
73
|
+
return client;
|
|
105
74
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (!options.openRouterApiKey?.trim())
|
|
111
|
-
return;
|
|
112
|
-
const routingEnv = options.routingEnv ?? loadOpenRouterRoutingEnv();
|
|
113
|
-
const viaOpenRouter = options.resolution?.routedViaOpenRouter === true ||
|
|
114
|
-
(options.resolution?.routedViaOpenRouter !== false &&
|
|
115
|
-
isEffectiveOpenRouterTransport(routingEnv, {
|
|
116
|
-
provider: merged.provider,
|
|
117
|
-
modelId: merged.model,
|
|
118
|
-
routeViaOpenRouter: options.preferOpenRouter ? true : undefined,
|
|
119
|
-
}));
|
|
120
|
-
if (viaOpenRouter) {
|
|
121
|
-
merged.allowOpenRouterProxy = true;
|
|
122
|
-
if (merged.provider && merged.provider !== 'openrouter') {
|
|
123
|
-
merged.providerProxy = 'openrouter';
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
merged.allowOpenRouterProxy = false;
|
|
128
|
-
}
|
|
75
|
+
/** Reset singleton (tests). */
|
|
76
|
+
export function resetAiToolsClientForTests() {
|
|
77
|
+
resetAiToolsInvokeClientForTestsUpstream();
|
|
78
|
+
bootstrapFailedLogged = false;
|
|
129
79
|
}
|
|
@@ -1,44 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @x12i/ai-tools invoke client bootstrap for the gateway.
|
|
3
|
+
* Model resolution orchestration lives in ai-tools ≥ 2.5.0 (`resolveInvokeModel`).
|
|
3
4
|
*/
|
|
4
|
-
import {
|
|
5
|
+
import { type AiToolsInvokeClient, type ModelResolutionSuccess, type OpenRouterRoutingConfig } from '@x12i/ai-tools';
|
|
5
6
|
import type { Logxer } from '@x12i/logxer';
|
|
6
|
-
import type {
|
|
7
|
-
export type AiToolsClientBundle =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import type { GatewayConfig } from './types.js';
|
|
8
|
+
export type AiToolsClientBundle = AiToolsInvokeClient;
|
|
9
|
+
export { resolveInvokeModel, applyOpenRouterInvokePolicy, buildInvokeModelResolverOptions, enrichModelResolutionError, mapResolutionToRouterConfig, ModelProfileUnroutableError, ModelProfileInputRejectedError, MODEL_PROFILE_UNROUTABLE, getAiToolsInvokeClient, resetAiToolsInvokeClientForTests as resetAiToolsInvokeClientForTestsUpstream, createAiToolsInvokeClient, } from '@x12i/ai-tools';
|
|
10
|
+
export { resolveOpenRouterApiKey, resolvePreferOpenRouter, readPreferOpenRouterFromEnv } from './openrouter-routing.js';
|
|
11
|
+
export type { InvokeModelResolutionDiagnostics, InvokeModelResolutionInput, InvokeModelResolutionOptions, InvokeModelResolutionResult, InvokeRouterConfigSlice, AiToolsInvokeClient, } from '@x12i/ai-tools';
|
|
12
|
+
/** @deprecated Use buildInvokeModelResolverOptions */
|
|
13
|
+
export declare function buildModelResolverOptions(config: GatewayConfig, routingEnv?: OpenRouterRoutingConfig): import("@x12i/ai-tools").ModelResolverOptions;
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
15
|
+
* @deprecated Use mapResolutionToRouterConfig from @x12i/ai-tools
|
|
14
16
|
*/
|
|
15
|
-
export declare function
|
|
17
|
+
export declare function applyModelResolution(merged: {
|
|
18
|
+
provider?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
}, resolution: ModelResolutionSuccess, gatewayDefaultEngine?: string, inputModel?: string): void;
|
|
16
21
|
/**
|
|
17
22
|
* Returns catalog + calculator, or null when disabled or bootstrap fails.
|
|
18
23
|
*/
|
|
19
24
|
export declare function getAiToolsClient(config: GatewayConfig, logger: Logxer): Promise<AiToolsClientBundle | null>;
|
|
20
25
|
/** Reset singleton (tests). */
|
|
21
26
|
export declare function resetAiToolsClientForTests(): void;
|
|
22
|
-
/**
|
|
23
|
-
* Map catalog resolution to router `{ provider, model }` (agnostic to openrouter vs vendor input).
|
|
24
|
-
*/
|
|
25
|
-
export declare function applyModelResolution(merged: NonNullable<ChatRequest['config']>, resolution: ModelResolutionSuccess, gatewayDefaultEngine?: string, inputModel?: string): void;
|
|
26
|
-
type RouterConfigSlice = {
|
|
27
|
-
provider?: string;
|
|
28
|
-
model?: string;
|
|
29
|
-
allowOpenRouterProxy?: boolean;
|
|
30
|
-
providerProxy?: string;
|
|
31
|
-
};
|
|
32
|
-
type ModelResolutionMeta = {
|
|
33
|
-
routedViaOpenRouter?: boolean;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Router invoke flags after mergeConfig + ai-tools resolution (OpenRouter vs direct transport).
|
|
37
|
-
*/
|
|
38
|
-
export declare function applyOpenRouterInvokePolicy(merged: RouterConfigSlice, options: {
|
|
39
|
-
openRouterApiKey?: string;
|
|
40
|
-
preferOpenRouter?: boolean;
|
|
41
|
-
routingEnv?: OpenRouterRoutingConfig;
|
|
42
|
-
resolution?: ModelResolutionMeta;
|
|
43
|
-
}): void;
|
|
44
|
-
export {};
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as crypto from 'crypto';
|
|
6
6
|
import { FallbackExhaustedError } from '@x12i/ai-providers-router';
|
|
7
|
-
import { ModelResolutionError,
|
|
7
|
+
import { ModelResolutionError, ModelProfileInputRejectedError, ModelProfileUnroutableError, resolveInvokeModel, } from '@x12i/ai-tools';
|
|
8
8
|
import { extractHttpStatusCode } from './gateway-retry.js';
|
|
9
9
|
import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
|
|
10
10
|
import { MaxTokensRequiredError, ModelRequiredError } from './instruction-errors.js';
|
|
11
|
-
import { applyModelResolution, buildModelResolverOptions } from './ai-tools-client.js';
|
|
12
11
|
import { DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, GATEWAY_DEFAULT_FREQUENCY_PENALTY, GATEWAY_DEFAULT_PRESENCE_PENALTY, GATEWAY_DEFAULT_TEMPERATURE, GATEWAY_DEFAULT_TOP_P } from './gateway-defaults.js';
|
|
13
12
|
function getPreParsedInstructions(instructions) {
|
|
14
13
|
return instructions ?? '';
|
|
@@ -37,6 +36,7 @@ export async function ensureTaskTypeId(request, logger) {
|
|
|
37
36
|
});
|
|
38
37
|
return taskTypeId;
|
|
39
38
|
}
|
|
39
|
+
export { MODEL_PROFILE_UNROUTABLE, ModelProfileUnroutableError, ModelProfileInputRejectedError, } from '@x12i/ai-tools';
|
|
40
40
|
/**
|
|
41
41
|
* Merges config with defaults
|
|
42
42
|
* Supports using internal system action defaults (internalSkill or skillAudit) when useInternalDefaults is set
|
|
@@ -100,45 +100,64 @@ export async function mergeConfig(request, config, logger, mergeOptions) {
|
|
|
100
100
|
}
|
|
101
101
|
if (resolveModels && mergeOptions?.catalog) {
|
|
102
102
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
logger.verbose('Catalog resolved model name', {
|
|
119
|
-
jobId: request.identity.jobId,
|
|
120
|
-
originalModel,
|
|
121
|
-
resolvedModelId: resolution.modelId,
|
|
122
|
-
provider: merged.provider,
|
|
123
|
-
model: merged.model,
|
|
124
|
-
confidence: resolution.confidence,
|
|
125
|
-
resolvedVia: resolution.resolvedVia
|
|
126
|
-
});
|
|
103
|
+
const resolved = await resolveInvokeModel({ provider: merged.provider, model: explicitModel }, {
|
|
104
|
+
catalog: mergeOptions.catalog,
|
|
105
|
+
routingEnv: mergeOptions.routingEnv,
|
|
106
|
+
openRouterApiKey: mergeOptions.openRouterApiKey,
|
|
107
|
+
preferOpenRouter: mergeOptions.preferOpenRouter,
|
|
108
|
+
defaultProvider: config.defaultEngine,
|
|
109
|
+
resolveModels: true,
|
|
110
|
+
modelsOnly: config.aiTools?.modelsOnly !== false,
|
|
111
|
+
...(config.aiTools?.catalogLane ? { catalogLane: config.aiTools.catalogLane } : {}),
|
|
112
|
+
...(config.aiTools?.bundledOnly ? { bundledOnly: true } : {}),
|
|
113
|
+
});
|
|
114
|
+
merged.provider = resolved.router.provider;
|
|
115
|
+
merged.model = resolved.router.model;
|
|
116
|
+
if (resolved.router.allowOpenRouterProxy !== undefined) {
|
|
117
|
+
merged.allowOpenRouterProxy = resolved.router.allowOpenRouterProxy;
|
|
127
118
|
}
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
if (resolved.router.providerProxy !== undefined) {
|
|
120
|
+
merged.providerProxy = resolved.router.providerProxy;
|
|
130
121
|
}
|
|
122
|
+
request._modelResolution = resolved.diagnostics;
|
|
123
|
+
logger.verbose('Catalog resolved model name', {
|
|
124
|
+
jobId: request.identity.jobId,
|
|
125
|
+
originalModel,
|
|
126
|
+
originalProvider,
|
|
127
|
+
resolvedModelId: resolved.diagnostics.modelId,
|
|
128
|
+
provider: merged.provider,
|
|
129
|
+
model: merged.model,
|
|
130
|
+
confidence: resolved.diagnostics.confidence,
|
|
131
|
+
resolvedVia: resolved.diagnostics.resolvedVia,
|
|
132
|
+
routedViaOpenRouter: resolved.diagnostics.routedViaOpenRouter,
|
|
133
|
+
});
|
|
131
134
|
}
|
|
132
135
|
catch (error) {
|
|
133
|
-
if (error instanceof ModelResolutionError
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (error instanceof ModelProfileUnroutableError) {
|
|
136
|
+
if (error instanceof ModelResolutionError ||
|
|
137
|
+
error instanceof ModelProfileUnroutableError ||
|
|
138
|
+
error instanceof ModelProfileInputRejectedError) {
|
|
137
139
|
throw error;
|
|
138
140
|
}
|
|
139
141
|
throw error;
|
|
140
142
|
}
|
|
141
143
|
}
|
|
144
|
+
else if (mergeOptions?.openRouterApiKey) {
|
|
145
|
+
const resolved = await resolveInvokeModel({ provider: merged.provider, model: explicitModel }, {
|
|
146
|
+
resolveModels: false,
|
|
147
|
+
routingEnv: mergeOptions.routingEnv,
|
|
148
|
+
openRouterApiKey: mergeOptions.openRouterApiKey,
|
|
149
|
+
preferOpenRouter: mergeOptions.preferOpenRouter,
|
|
150
|
+
defaultProvider: config.defaultEngine,
|
|
151
|
+
});
|
|
152
|
+
merged.provider = resolved.router.provider;
|
|
153
|
+
merged.model = resolved.router.model;
|
|
154
|
+
if (resolved.router.allowOpenRouterProxy !== undefined) {
|
|
155
|
+
merged.allowOpenRouterProxy = resolved.router.allowOpenRouterProxy;
|
|
156
|
+
}
|
|
157
|
+
if (resolved.router.providerProxy !== undefined) {
|
|
158
|
+
merged.providerProxy = resolved.router.providerProxy;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
142
161
|
if (!merged.model) {
|
|
143
162
|
throw new ModelRequiredError();
|
|
144
163
|
}
|
|
@@ -351,42 +370,119 @@ export function resolveCostCompletionForActivity(routerResponse, tokens) {
|
|
|
351
370
|
}
|
|
352
371
|
return resolveActivityCostCompletion(tokens, costUsd);
|
|
353
372
|
}
|
|
354
|
-
/**
|
|
373
|
+
/**
|
|
374
|
+
* Best-effort cache/reasoning token counts from router usage buckets
|
|
375
|
+
* (for {@link buildGatewayPricingRecord} / ai-tools {@link CostCalculator.calculateFromRecord}).
|
|
376
|
+
*/
|
|
377
|
+
export function extractUsageExtrasFromRouterResponse(routerResponse) {
|
|
378
|
+
if (routerResponse == null || typeof routerResponse !== 'object')
|
|
379
|
+
return {};
|
|
380
|
+
const r = routerResponse;
|
|
381
|
+
const roots = [r.usage];
|
|
382
|
+
const meta = r.metadata != null && typeof r.metadata === 'object'
|
|
383
|
+
? r.metadata
|
|
384
|
+
: undefined;
|
|
385
|
+
if (meta) {
|
|
386
|
+
roots.push(meta.usage, meta.tokens);
|
|
387
|
+
}
|
|
388
|
+
const raw = r.rawResponse ?? r.raw;
|
|
389
|
+
if (raw != null && typeof raw === 'object') {
|
|
390
|
+
roots.push(raw.usage);
|
|
391
|
+
}
|
|
392
|
+
const extras = {};
|
|
393
|
+
for (const bucket of roots) {
|
|
394
|
+
if (bucket == null || typeof bucket !== 'object')
|
|
395
|
+
continue;
|
|
396
|
+
const u = bucket;
|
|
397
|
+
const cached = firstFiniteNumber(u.cached, u.cached_tokens, u.cachedTokens, u.cache_read_tokens, u.cacheReadTokens);
|
|
398
|
+
const cacheWrite = firstFiniteNumber(u.cacheWrite, u.cache_write_tokens, u.cacheWriteTokens);
|
|
399
|
+
const reasoning = firstFiniteNumber(u.reasoning, u.reasoning_tokens, u.reasoningTokens);
|
|
400
|
+
if (cached !== undefined && extras.cached === undefined)
|
|
401
|
+
extras.cached = cached;
|
|
402
|
+
if (cacheWrite !== undefined && extras.cacheWrite === undefined)
|
|
403
|
+
extras.cacheWrite = cacheWrite;
|
|
404
|
+
if (reasoning !== undefined && extras.reasoning === undefined)
|
|
405
|
+
extras.reasoning = reasoning;
|
|
406
|
+
}
|
|
407
|
+
return extras;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Whether ai-tools catalog pricing is authoritative enough for Step B (`priced`).
|
|
411
|
+
* Matches the generic engine contract: authoritative catalog hit with finite cost ≥ 0.
|
|
412
|
+
*/
|
|
413
|
+
export function catalogPricingSucceeded(result) {
|
|
414
|
+
if (result.unknownModel)
|
|
415
|
+
return false;
|
|
416
|
+
if (!result.isAuthoritative)
|
|
417
|
+
return false;
|
|
418
|
+
if (result.source === 'estimate-fallback' || result.source === 'local')
|
|
419
|
+
return false;
|
|
420
|
+
if (typeof result.cost !== 'number' || !Number.isFinite(result.cost) || result.cost < 0) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
/** Record shape for {@link CostCalculator.calculateFromRecord} (shared engine contract). */
|
|
355
426
|
export function buildGatewayPricingRecord(routerResponse, tokens, mergedConfig) {
|
|
356
|
-
const base = routerResponse != null && typeof routerResponse === 'object'
|
|
357
|
-
? { ...routerResponse }
|
|
358
|
-
: {};
|
|
359
|
-
const meta = base.metadata != null && typeof base.metadata === 'object'
|
|
360
|
-
? { ...base.metadata }
|
|
361
|
-
: {};
|
|
362
427
|
const routing = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
|
|
428
|
+
const cfg = mergedConfig != null && typeof mergedConfig === 'object'
|
|
429
|
+
? mergedConfig
|
|
430
|
+
: {};
|
|
431
|
+
const requestModel = typeof cfg.model === 'string'
|
|
432
|
+
? cfg.model
|
|
433
|
+
: typeof routing.modelUsed === 'string'
|
|
434
|
+
? routing.modelUsed
|
|
435
|
+
: undefined;
|
|
436
|
+
const modelUsed = routing.modelUsed ?? requestModel;
|
|
437
|
+
const provider = routing.provider ??
|
|
438
|
+
(typeof cfg.provider === 'string' ? cfg.provider : undefined) ??
|
|
439
|
+
'openrouter';
|
|
440
|
+
const usageExtras = extractUsageExtrasFromRouterResponse(routerResponse);
|
|
441
|
+
const tokenSlice = {
|
|
442
|
+
prompt: tokens.prompt,
|
|
443
|
+
completion: tokens.completion,
|
|
444
|
+
total: tokens.total,
|
|
445
|
+
...usageExtras
|
|
446
|
+
};
|
|
363
447
|
return {
|
|
364
|
-
|
|
448
|
+
model: modelUsed ?? requestModel ?? '',
|
|
449
|
+
...(requestModel && modelUsed && requestModel !== modelUsed
|
|
450
|
+
? { modelAlias: requestModel }
|
|
451
|
+
: {}),
|
|
452
|
+
...(modelUsed ? { modelUsed, usedModel: modelUsed } : {}),
|
|
453
|
+
provider,
|
|
454
|
+
...(provider || routing.region
|
|
455
|
+
? {
|
|
456
|
+
routing: {
|
|
457
|
+
provider,
|
|
458
|
+
...(routing.region ? { region: routing.region } : {})
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
: {}),
|
|
365
462
|
usage: {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
463
|
+
prompt_tokens: tokens.prompt,
|
|
464
|
+
completion_tokens: tokens.completion,
|
|
465
|
+
total_tokens: tokens.total,
|
|
466
|
+
...(usageExtras.cached !== undefined ? { cachedTokensPrompt: usageExtras.cached } : {}),
|
|
467
|
+
...(usageExtras.cached !== undefined ? { cachedTokensTotal: usageExtras.cached } : {})
|
|
369
468
|
},
|
|
370
|
-
tokens,
|
|
469
|
+
promptTokens: tokens.prompt,
|
|
470
|
+
completionTokens: tokens.completion,
|
|
471
|
+
totalTokens: tokens.total,
|
|
472
|
+
tokens: tokenSlice,
|
|
371
473
|
metadata: {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
...(routing.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
474
|
+
provider,
|
|
475
|
+
...(modelUsed ? { modelUsed, model: modelUsed } : {}),
|
|
476
|
+
...(routing.maxTokensRequested !== undefined
|
|
477
|
+
? { maxTokensRequested: routing.maxTokensRequested }
|
|
478
|
+
: {}),
|
|
479
|
+
tokens: tokenSlice
|
|
378
480
|
},
|
|
379
481
|
...(mergedConfig != null ? { config: mergedConfig } : {})
|
|
380
482
|
};
|
|
381
483
|
}
|
|
382
484
|
export function mapAiCostResultToResolvedActivityCost(base, result) {
|
|
383
|
-
if (result
|
|
384
|
-
return base.costStatus ? base : { ...base, costStatus: 'unpriced' };
|
|
385
|
-
}
|
|
386
|
-
if (typeof result.cost !== 'number' || !Number.isFinite(result.cost)) {
|
|
387
|
-
return base;
|
|
388
|
-
}
|
|
389
|
-
if (!result.isAuthoritative && result.source === 'estimate-fallback') {
|
|
485
|
+
if (!catalogPricingSucceeded(result)) {
|
|
390
486
|
return base.costStatus ? base : { ...base, costStatus: 'unpriced' };
|
|
391
487
|
}
|
|
392
488
|
return {
|
|
@@ -395,6 +491,16 @@ export function mapAiCostResultToResolvedActivityCost(base, result) {
|
|
|
395
491
|
...(result.breakdown ? { costBreakdown: result.breakdown } : {})
|
|
396
492
|
};
|
|
397
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* G8 safety net: token usage without a billing signal → `unpriced`.
|
|
496
|
+
* Used at invoke boundaries after {@link resolveCostCompletionWithAiTools}.
|
|
497
|
+
*/
|
|
498
|
+
export function ensureInvokeBillingCostStatus(billing, tokens) {
|
|
499
|
+
if (!billing.costStatus && hasNonZeroTokenUsage(tokens)) {
|
|
500
|
+
return { ...billing, costStatus: 'unpriced' };
|
|
501
|
+
}
|
|
502
|
+
return billing;
|
|
503
|
+
}
|
|
398
504
|
/**
|
|
399
505
|
* Router cost passthrough, then optional @x12i/ai-tools catalog pricing when still unpriced.
|
|
400
506
|
*/
|
|
@@ -419,30 +525,7 @@ export async function resolveCostCompletionWithAiTools(routerResponse, tokens, o
|
|
|
419
525
|
return mapAiCostResultToResolvedActivityCost(base, result);
|
|
420
526
|
}
|
|
421
527
|
catch {
|
|
422
|
-
|
|
423
|
-
const cfg = options.mergedConfig != null && typeof options.mergedConfig === 'object'
|
|
424
|
-
? options.mergedConfig
|
|
425
|
-
: {};
|
|
426
|
-
const provider = routing.provider ?? cfg.provider;
|
|
427
|
-
const modelUsed = routing.modelUsed ?? cfg.model;
|
|
428
|
-
if (!provider || !modelUsed) {
|
|
429
|
-
return base;
|
|
430
|
-
}
|
|
431
|
-
try {
|
|
432
|
-
const result = await options.calculator.calculate({
|
|
433
|
-
tokens: {
|
|
434
|
-
prompt: tokens.prompt,
|
|
435
|
-
completion: tokens.completion,
|
|
436
|
-
total: tokens.total
|
|
437
|
-
},
|
|
438
|
-
provider,
|
|
439
|
-
usedModel: modelUsed
|
|
440
|
-
});
|
|
441
|
-
return mapAiCostResultToResolvedActivityCost(base, result);
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
return base;
|
|
445
|
-
}
|
|
528
|
+
return ensureInvokeBillingCostStatus(base, tokens);
|
|
446
529
|
}
|
|
447
530
|
}
|
|
448
531
|
function applyBillingToTraceAttempt(attempt, billing) {
|
|
@@ -679,31 +762,6 @@ export function pickRequestIdsFromRouterLike(gatewayAiRequestId, routerLike) {
|
|
|
679
762
|
}
|
|
680
763
|
return out;
|
|
681
764
|
}
|
|
682
|
-
/** Error code hint when a bundled profile name cannot be routed to a catalog target. */
|
|
683
|
-
export const MODEL_PROFILE_UNROUTABLE = 'MODEL_PROFILE_UNROUTABLE';
|
|
684
|
-
export class ModelProfileUnroutableError extends Error {
|
|
685
|
-
profileAlias;
|
|
686
|
-
provider;
|
|
687
|
-
code = MODEL_PROFILE_UNROUTABLE;
|
|
688
|
-
constructor(profileAlias, provider, cause) {
|
|
689
|
-
super(`${MODEL_PROFILE_UNROUTABLE}: profile "${profileAlias}" is retired or has no routable catalog target` +
|
|
690
|
-
(provider ? ` (provider: "${provider}")` : '') +
|
|
691
|
-
'. Update @x12i/ai-profiles or choose another profile alias.');
|
|
692
|
-
this.profileAlias = profileAlias;
|
|
693
|
-
this.provider = provider;
|
|
694
|
-
this.name = 'ModelProfileUnroutableError';
|
|
695
|
-
if (cause !== undefined) {
|
|
696
|
-
this.cause = cause;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
function buildModelResolutionFailureError(explicitModel, provider, resolution) {
|
|
701
|
-
const base = new ModelResolutionError({ provider, model: explicitModel }, resolution);
|
|
702
|
-
if (isKnownProfileChoice(explicitModel)) {
|
|
703
|
-
return new ModelProfileUnroutableError(explicitModel, provider, base);
|
|
704
|
-
}
|
|
705
|
-
return base;
|
|
706
|
-
}
|
|
707
765
|
/**
|
|
708
766
|
* Build rejection-metadata fallback attempts from trace-mode {@link GatewayTraceAttempt}s.
|
|
709
767
|
*/
|
|
@@ -772,7 +830,7 @@ export function mapGatewayFallbackAttemptsToRouter(attempts) {
|
|
|
772
830
|
}));
|
|
773
831
|
}
|
|
774
832
|
/**
|
|
775
|
-
* Log
|
|
833
|
+
* Log original input vs OpenRouter model id actually sent to the router after catalog resolution.
|
|
776
834
|
*/
|
|
777
835
|
export function logResolvedModelRouting(logger, request, mergedConfig) {
|
|
778
836
|
const res = request._modelResolution;
|
|
@@ -780,14 +838,14 @@ export function logResolvedModelRouting(logger, request, mergedConfig) {
|
|
|
780
838
|
return;
|
|
781
839
|
}
|
|
782
840
|
const profileAlias = res.originalModel ?? mergedConfig?.model;
|
|
783
|
-
const invokedModelId = res.modelId ?? mergedConfig?.model;
|
|
841
|
+
const invokedModelId = res.invokedModelId ?? res.modelId ?? mergedConfig?.model;
|
|
784
842
|
const provider = mergedConfig?.provider;
|
|
785
843
|
const openRouterPath = res.routedViaOpenRouter === true || provider === 'openrouter';
|
|
786
844
|
if (!openRouterPath) {
|
|
787
845
|
return;
|
|
788
846
|
}
|
|
789
|
-
logger.info('OpenRouter routing:
|
|
790
|
-
profileAlias,
|
|
847
|
+
logger.info('OpenRouter routing: input model resolved to catalog id for invoke', withActivityIdentity(request.identity, {
|
|
848
|
+
inputModel: profileAlias,
|
|
791
849
|
invokedOpenRouterModelId: invokedModelId,
|
|
792
850
|
provider,
|
|
793
851
|
routedViaOpenRouter: res.routedViaOpenRouter,
|