@x12i/ai-gateway 9.1.6 → 9.3.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 +45 -0
- package/dist/activity-manager.d.ts +1 -0
- package/dist/activity-manager.js +7 -0
- package/dist/ai-tools-client.d.ts +20 -0
- package/dist/ai-tools-client.js +91 -0
- package/dist/flex-md-loader.d.ts +5 -0
- package/dist/flex-md-loader.js +16 -0
- package/dist/gateway-config.d.ts +2 -0
- package/dist/gateway-config.js +2 -1
- package/dist/gateway-mode.d.ts +40 -0
- package/dist/gateway-mode.js +75 -0
- package/dist/gateway-utils.d.ts +57 -1
- package/dist/gateway-utils.js +181 -12
- package/dist/gateway.d.ts +3 -0
- package/dist/gateway.js +47 -15
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/dist/output-contract-normalizer.d.ts +21 -0
- package/dist/output-contract-normalizer.js +121 -0
- package/dist/types.d.ts +35 -0
- package/dist-cjs/activity-manager.cjs +21 -19
- package/dist-cjs/activity-manager.d.ts +1 -0
- package/dist-cjs/ai-tools-client.cjs +91 -0
- package/dist-cjs/ai-tools-client.d.ts +20 -0
- package/dist-cjs/config/activity-tracking-config.cjs +1 -4
- package/dist-cjs/content-normalizer/content-normalizer.cjs +3 -8
- package/dist-cjs/content-normalizer/index.cjs +1 -7
- package/dist-cjs/content-normalizer/types.cjs +1 -2
- package/dist-cjs/flex-md-loader.cjs +35 -65
- package/dist-cjs/flex-md-loader.d.ts +5 -0
- package/dist-cjs/gateway-config.cjs +25 -63
- package/dist-cjs/gateway-config.d.ts +2 -0
- package/dist-cjs/gateway-conversion.cjs +10 -48
- package/dist-cjs/gateway-instructions.cjs +5 -10
- package/dist-cjs/gateway-log-meta.cjs +9 -14
- package/dist-cjs/gateway-memory.cjs +2 -6
- package/dist-cjs/gateway-messages.cjs +3 -6
- package/dist-cjs/gateway-meta.cjs +1 -4
- package/dist-cjs/gateway-mode.cjs +75 -0
- package/dist-cjs/gateway-mode.d.ts +40 -0
- package/dist-cjs/gateway-provider-auto-register.cjs +2 -38
- package/dist-cjs/gateway-provider.cjs +10 -22
- package/dist-cjs/gateway-rate-limiter-constants.cjs +2 -5
- package/dist-cjs/gateway-rate-limiter.cjs +5 -9
- package/dist-cjs/gateway-retry.cjs +6 -14
- package/dist-cjs/gateway-utils.cjs +201 -83
- package/dist-cjs/gateway-utils.d.ts +57 -1
- package/dist-cjs/gateway-validation.cjs +2 -6
- package/dist-cjs/gateway.cjs +100 -72
- package/dist-cjs/gateway.d.ts +3 -0
- package/dist-cjs/index.cjs +22 -91
- package/dist-cjs/index.d.ts +6 -1
- package/dist-cjs/instruction-errors.cjs +2 -7
- package/dist-cjs/instruction-optimizer.cjs +4 -10
- package/dist-cjs/instructions-parser.cjs +5 -10
- package/dist-cjs/logger-factory.cjs +3 -6
- package/dist-cjs/memory-path-resolution.cjs +8 -18
- package/dist-cjs/message-builder.cjs +11 -47
- package/dist-cjs/object-types-library-integration.cjs +3 -8
- package/dist-cjs/object-types-library.cjs +5 -10
- package/dist-cjs/output-auditor.cjs +1 -4
- package/dist-cjs/output-contract-normalizer.cjs +121 -0
- package/dist-cjs/output-contract-normalizer.d.ts +21 -0
- package/dist-cjs/request-report-generator.cjs +1 -4
- package/dist-cjs/response-analyzer/format-type-detector.cjs +1 -5
- package/dist-cjs/response-analyzer/index.cjs +3 -9
- package/dist-cjs/response-analyzer/object-type-detector.cjs +1 -5
- package/dist-cjs/response-analyzer/response-analyzer.cjs +6 -10
- package/dist-cjs/response-analyzer/types.cjs +1 -2
- package/dist-cjs/response-fallback-fixer.cjs +1 -4
- package/dist-cjs/runtime-objects.cjs +7 -13
- package/dist-cjs/template-parser.cjs +5 -42
- package/dist-cjs/template-render-merge.cjs +2 -6
- package/dist-cjs/troubleshooting-helper.cjs +13 -28
- package/dist-cjs/types.cjs +1 -2
- package/dist-cjs/types.d.ts +35 -0
- package/dist-cjs/usage-tracker.cjs +3 -7
- package/package.json +11 -5
package/dist/gateway-utils.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
* Handles utility functions
|
|
4
4
|
*/
|
|
5
5
|
import * as crypto from 'crypto';
|
|
6
|
+
import { ModelResolutionError } from '@x12i/ai-tools';
|
|
6
7
|
import { getPreParsedInstructions } from './gateway-instructions.js';
|
|
7
8
|
import { getModelMaxTokensFromFlexMd } from './flex-md-loader.js';
|
|
9
|
+
import { applyModelResolution } from './ai-tools-client.js';
|
|
10
|
+
import { getGatewayOperationalMode, isProdGatewayMode, resolveGatewayDefaultModel, warnDefaultModelSubstitution } from './gateway-mode.js';
|
|
8
11
|
/**
|
|
9
12
|
* Generates MD5 hash of a string
|
|
10
13
|
*/
|
|
@@ -29,11 +32,34 @@ export async function ensureTaskTypeId(request, logger) {
|
|
|
29
32
|
});
|
|
30
33
|
return taskTypeId;
|
|
31
34
|
}
|
|
35
|
+
function applyGatewayDefaultToMerged(merged, defaults, config) {
|
|
36
|
+
merged.model = defaults.model;
|
|
37
|
+
if (defaults.provider) {
|
|
38
|
+
merged.provider = defaults.provider;
|
|
39
|
+
}
|
|
40
|
+
else if (!merged.provider) {
|
|
41
|
+
merged.provider = config.defaultEngine;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, reason, original) {
|
|
45
|
+
const operationalMode = getGatewayOperationalMode(config);
|
|
46
|
+
const defaults = resolveGatewayDefaultModel(mergeOptions?.defaultModelConfig, config.defaultEngine);
|
|
47
|
+
warnDefaultModelSubstitution(logger, request.identity, {
|
|
48
|
+
reason,
|
|
49
|
+
mode: operationalMode,
|
|
50
|
+
defaultSource: defaults.source,
|
|
51
|
+
defaultProvider: defaults.provider ?? merged.provider,
|
|
52
|
+
defaultModel: defaults.model,
|
|
53
|
+
originalProvider: original?.provider ?? merged.provider,
|
|
54
|
+
originalModel: original?.model
|
|
55
|
+
});
|
|
56
|
+
applyGatewayDefaultToMerged(merged, defaults, config);
|
|
57
|
+
}
|
|
32
58
|
/**
|
|
33
59
|
* Merges config with defaults
|
|
34
60
|
* Supports using internal system action defaults (internalSkill or skillAudit) when useInternalDefaults is set
|
|
35
61
|
*/
|
|
36
|
-
export async function mergeConfig(request, config, logger) {
|
|
62
|
+
export async function mergeConfig(request, config, logger, mergeOptions) {
|
|
37
63
|
const useInternalDefaults = request.useInternalDefaults;
|
|
38
64
|
const internalDefaults = useInternalDefaults
|
|
39
65
|
? (useInternalDefaults === 'skill'
|
|
@@ -52,8 +78,8 @@ export async function mergeConfig(request, config, logger) {
|
|
|
52
78
|
useInternalDefaults,
|
|
53
79
|
hasInternalDefaults: !!internalDefaults
|
|
54
80
|
});
|
|
55
|
-
|
|
56
|
-
const
|
|
81
|
+
const operationalMode = getGatewayOperationalMode(config);
|
|
82
|
+
const resolveModels = config.aiTools?.resolveModels !== false;
|
|
57
83
|
// Priority: modelConfig > request.config > internalSystemActions[useInternalDefaults] > gateway defaults
|
|
58
84
|
// First, merge modelConfig into a config-like object if present
|
|
59
85
|
const modelConfigAsConfig = request.modelConfig ? {
|
|
@@ -87,18 +113,67 @@ export async function mergeConfig(request, config, logger) {
|
|
|
87
113
|
...request.config,
|
|
88
114
|
// ModelConfig overrides (highest priority) - merge only defined values
|
|
89
115
|
...(modelConfigAsConfig ? Object.fromEntries(Object.entries(modelConfigAsConfig).filter(([_, value]) => value !== undefined)) : {}),
|
|
90
|
-
//
|
|
91
|
-
model: modelConfigAsConfig?.model || request.config?.model || internalDefaults?.model
|
|
116
|
+
// Model resolved below (catalog, default chain, or explicit pass-through)
|
|
117
|
+
model: modelConfigAsConfig?.model || request.config?.model || internalDefaults?.model,
|
|
92
118
|
// Ensure provider is set: modelConfig > request.config > internalDefaults > gateway default
|
|
93
|
-
// Provider is required for router to know which provider to use
|
|
94
119
|
provider: modelConfigAsConfig?.provider || request.config?.provider || internalDefaults?.engine || config.defaultEngine
|
|
95
120
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
const explicitModel = merged.model;
|
|
122
|
+
const originalProvider = merged.provider;
|
|
123
|
+
const originalModel = explicitModel;
|
|
124
|
+
if (!explicitModel) {
|
|
125
|
+
await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'no_model_provided');
|
|
126
|
+
}
|
|
127
|
+
else if (resolveModels && mergeOptions?.catalog) {
|
|
128
|
+
try {
|
|
129
|
+
const resolution = await mergeOptions.catalog.resolveModel({
|
|
130
|
+
provider: merged.provider,
|
|
131
|
+
model: explicitModel
|
|
132
|
+
});
|
|
133
|
+
if (resolution.found) {
|
|
134
|
+
applyModelResolution(merged, resolution, config.defaultEngine);
|
|
135
|
+
request._modelResolution = {
|
|
136
|
+
modelId: resolution.modelId,
|
|
137
|
+
routedViaOpenRouter: resolution.routedViaOpenRouter,
|
|
138
|
+
confidence: resolution.confidence,
|
|
139
|
+
resolvedVia: resolution.resolvedVia,
|
|
140
|
+
originalProvider,
|
|
141
|
+
originalModel
|
|
142
|
+
};
|
|
143
|
+
logger.verbose('Catalog resolved model name', {
|
|
144
|
+
jobId: request.identity.jobId,
|
|
145
|
+
originalModel,
|
|
146
|
+
resolvedModelId: resolution.modelId,
|
|
147
|
+
provider: merged.provider,
|
|
148
|
+
model: merged.model,
|
|
149
|
+
confidence: resolution.confidence,
|
|
150
|
+
resolvedVia: resolution.resolvedVia
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else if (isProdGatewayMode(operationalMode)) {
|
|
154
|
+
await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'model_resolution_failed', { provider: originalProvider, model: originalModel });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
throw new ModelResolutionError({ provider: merged.provider, model: explicitModel }, resolution);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error instanceof ModelResolutionError) {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
if (isProdGatewayMode(operationalMode)) {
|
|
165
|
+
await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'ai_tools_unavailable', { provider: originalProvider, model: originalModel });
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (resolveModels && !mergeOptions?.catalog && isProdGatewayMode(operationalMode)) {
|
|
173
|
+
await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'ai_tools_unavailable', { provider: originalProvider, model: originalModel });
|
|
174
|
+
}
|
|
175
|
+
if (!merged.model) {
|
|
176
|
+
await substituteGatewayDefaultModel(merged, request, config, logger, mergeOptions, 'no_model_provided');
|
|
102
177
|
}
|
|
103
178
|
// Auto-get maxTokens from flex-md if not explicitly set in ANY config source
|
|
104
179
|
// Check all possible sources: request.config, internalDefaults, gateway config
|
|
@@ -315,6 +390,100 @@ export function extractCostUsdFromRouterResponse(routerResponse) {
|
|
|
315
390
|
}
|
|
316
391
|
return undefined;
|
|
317
392
|
}
|
|
393
|
+
export function hasNonZeroTokenUsage(tokens) {
|
|
394
|
+
return !!(tokens.prompt || tokens.completion || tokens.total);
|
|
395
|
+
}
|
|
396
|
+
function pickRouterCostStatus(routerResponse) {
|
|
397
|
+
if (routerResponse == null || typeof routerResponse !== 'object')
|
|
398
|
+
return undefined;
|
|
399
|
+
const r = routerResponse;
|
|
400
|
+
const meta = r.metadata != null && typeof r.metadata === 'object'
|
|
401
|
+
? r.metadata
|
|
402
|
+
: undefined;
|
|
403
|
+
const status = meta?.costStatus ?? r.costStatus;
|
|
404
|
+
return status === 'priced' || status === 'unpriced' ? status : undefined;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Gateway fallback when the router does not set `metadata.costStatus`.
|
|
408
|
+
* Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
|
|
409
|
+
*/
|
|
410
|
+
export function resolveActivityCostCompletion(tokens, costUsd) {
|
|
411
|
+
if (typeof costUsd === 'number' && Number.isFinite(costUsd)) {
|
|
412
|
+
return { cost: costUsd, costStatus: 'priced' };
|
|
413
|
+
}
|
|
414
|
+
if (hasNonZeroTokenUsage(tokens)) {
|
|
415
|
+
return { costStatus: 'unpriced' };
|
|
416
|
+
}
|
|
417
|
+
return {};
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
|
|
421
|
+
* otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
|
|
422
|
+
*/
|
|
423
|
+
export function resolveCostCompletionForActivity(routerResponse, tokens) {
|
|
424
|
+
const routerStatus = pickRouterCostStatus(routerResponse);
|
|
425
|
+
const costUsd = extractCostUsdFromRouterResponse(routerResponse);
|
|
426
|
+
if (routerStatus === 'priced') {
|
|
427
|
+
return {
|
|
428
|
+
...(typeof costUsd === 'number' && Number.isFinite(costUsd) ? { cost: costUsd } : {}),
|
|
429
|
+
costStatus: 'priced'
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (routerStatus === 'unpriced') {
|
|
433
|
+
return { costStatus: 'unpriced' };
|
|
434
|
+
}
|
|
435
|
+
return resolveActivityCostCompletion(tokens, costUsd);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Router cost passthrough, then optional @x12i/ai-tools catalog pricing when still unpriced.
|
|
439
|
+
*/
|
|
440
|
+
export async function resolveCostCompletionWithAiTools(routerResponse, tokens, options) {
|
|
441
|
+
const routerStatus = pickRouterCostStatus(routerResponse);
|
|
442
|
+
const base = resolveCostCompletionForActivity(routerResponse, tokens);
|
|
443
|
+
if (base.costStatus === 'priced') {
|
|
444
|
+
return base;
|
|
445
|
+
}
|
|
446
|
+
if (routerStatus === 'unpriced') {
|
|
447
|
+
return base;
|
|
448
|
+
}
|
|
449
|
+
if (options?.calculateCost === false || !options?.calculator) {
|
|
450
|
+
return base;
|
|
451
|
+
}
|
|
452
|
+
if (!hasNonZeroTokenUsage(tokens)) {
|
|
453
|
+
return base;
|
|
454
|
+
}
|
|
455
|
+
const routing = pickInvokeRoutingMetadataSlice(routerResponse, options.mergedConfig);
|
|
456
|
+
const cfg = options.mergedConfig != null && typeof options.mergedConfig === 'object'
|
|
457
|
+
? options.mergedConfig
|
|
458
|
+
: {};
|
|
459
|
+
const provider = routing.provider ?? cfg.provider;
|
|
460
|
+
const modelUsed = routing.modelUsed ?? cfg.model;
|
|
461
|
+
if (!provider || !modelUsed) {
|
|
462
|
+
return base;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const result = await options.calculator.calculate({
|
|
466
|
+
tokens: {
|
|
467
|
+
prompt: tokens.prompt,
|
|
468
|
+
completion: tokens.completion,
|
|
469
|
+
total: tokens.total
|
|
470
|
+
},
|
|
471
|
+
provider,
|
|
472
|
+
modelUsed
|
|
473
|
+
});
|
|
474
|
+
if (typeof result.cost === 'number' && Number.isFinite(result.cost)) {
|
|
475
|
+
return {
|
|
476
|
+
cost: result.cost,
|
|
477
|
+
costStatus: 'priced',
|
|
478
|
+
...(result.breakdown ? { costBreakdown: result.breakdown } : {})
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// Keep router/gateway unpriced fallback
|
|
484
|
+
}
|
|
485
|
+
return base;
|
|
486
|
+
}
|
|
318
487
|
/**
|
|
319
488
|
* Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
|
|
320
489
|
* Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
|
package/dist/gateway.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export declare class AIGateway {
|
|
|
16
16
|
private logger;
|
|
17
17
|
private activityManager?;
|
|
18
18
|
private messageBuilderConfig?;
|
|
19
|
+
private defaultModelConfig;
|
|
19
20
|
private _autoRegisterDone;
|
|
21
|
+
private _aiToolsClient;
|
|
20
22
|
constructor(config?: GatewayConfig, activityManager?: ActivityManager);
|
|
21
23
|
/**
|
|
22
24
|
* Invoke chat request (without structured output requirements)
|
|
@@ -36,4 +38,5 @@ export declare class AIGateway {
|
|
|
36
38
|
getLogger(): Logxer;
|
|
37
39
|
getActivityManager(): ActivityManager | undefined;
|
|
38
40
|
setActivityManager(activityManager: ActivityManager): void;
|
|
41
|
+
private getAiTools;
|
|
39
42
|
}
|
package/dist/gateway.js
CHANGED
|
@@ -8,7 +8,9 @@ import { ensureGatewayRequestIdentity } from './activity-manager.js';
|
|
|
8
8
|
import { initializeGatewayComponents } from './gateway-config.js';
|
|
9
9
|
import { buildMessages } from './message-builder.js';
|
|
10
10
|
import { extractJsonFromFlexMd } from './flex-md-loader.js';
|
|
11
|
-
import {
|
|
11
|
+
import { enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
|
|
12
|
+
import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, resolveCostCompletionWithAiTools, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
|
|
13
|
+
import { getAiToolsClient } from './ai-tools-client.js';
|
|
12
14
|
import { autoRegisterProviders } from './gateway-provider-auto-register.js';
|
|
13
15
|
import { setGatewayLastJobId, setGatewayRuntimeClients } from './runtime-objects.js';
|
|
14
16
|
import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
|
|
@@ -44,7 +46,9 @@ export class AIGateway {
|
|
|
44
46
|
logger;
|
|
45
47
|
activityManager;
|
|
46
48
|
messageBuilderConfig;
|
|
49
|
+
defaultModelConfig = {};
|
|
47
50
|
_autoRegisterDone = false;
|
|
51
|
+
_aiToolsClient = null;
|
|
48
52
|
constructor(config = {}, activityManager) {
|
|
49
53
|
this.config = config;
|
|
50
54
|
this.activityManager = activityManager;
|
|
@@ -53,6 +57,7 @@ export class AIGateway {
|
|
|
53
57
|
this.router = components.router;
|
|
54
58
|
this.activityManager = components.activityManager;
|
|
55
59
|
this.messageBuilderConfig = components.messageBuilderConfig;
|
|
60
|
+
this.defaultModelConfig = components.defaultModelConfig ?? {};
|
|
56
61
|
setGatewayRuntimeClients({
|
|
57
62
|
activix: this.activityManager?.getTracker(),
|
|
58
63
|
logger: this.logger
|
|
@@ -76,7 +81,11 @@ export class AIGateway {
|
|
|
76
81
|
// Simple message construction
|
|
77
82
|
const messages = this.buildSimpleMessages(request);
|
|
78
83
|
// Merge config (modelConfig > request.config > gateway defaults)
|
|
79
|
-
const
|
|
84
|
+
const aiTools = await this.getAiTools();
|
|
85
|
+
const mergedConfig = await mergeConfig(request, this.config, this.logger, {
|
|
86
|
+
defaultModelConfig: this.defaultModelConfig,
|
|
87
|
+
catalog: aiTools?.catalog ?? null
|
|
88
|
+
});
|
|
80
89
|
// Activix start snapshot must match what the router receives (modelConfig-only callers omit request.config.model).
|
|
81
90
|
request._mergedRouterConfig = mergedConfig;
|
|
82
91
|
// Lazy auto-register providers from env (OPENAI_API_KEY, etc.) so consumers don't have to call init
|
|
@@ -108,8 +117,13 @@ export class AIGateway {
|
|
|
108
117
|
},
|
|
109
118
|
mode: 'sync'
|
|
110
119
|
});
|
|
111
|
-
const costUsdChat = extractCostUsdFromRouterResponse(response);
|
|
112
120
|
const metaChat = response?.metadata || {};
|
|
121
|
+
const tokensChat = extractTokenUsageFromRouterResponse(response);
|
|
122
|
+
const costCompletionChat = await resolveCostCompletionWithAiTools(response, tokensChat, {
|
|
123
|
+
mergedConfig,
|
|
124
|
+
calculator: aiTools?.calculator ?? null,
|
|
125
|
+
calculateCost: this.config.aiTools?.calculateCost
|
|
126
|
+
});
|
|
113
127
|
// Create enhanced response
|
|
114
128
|
const enhancedResponse = {
|
|
115
129
|
content: response.content || '',
|
|
@@ -117,22 +131,25 @@ export class AIGateway {
|
|
|
117
131
|
aiRequestId: request.aiRequestId,
|
|
118
132
|
identity: request.identity,
|
|
119
133
|
latencyMs: Date.now() - startTime,
|
|
120
|
-
tokens:
|
|
134
|
+
tokens: tokensChat,
|
|
121
135
|
taskTypeId,
|
|
122
136
|
agentType: 'chat',
|
|
123
|
-
...(
|
|
137
|
+
...(costCompletionChat.costStatus === 'priced'
|
|
124
138
|
? {
|
|
125
|
-
costUsd:
|
|
126
|
-
...(typeof metaChat.cost === 'number'
|
|
139
|
+
costUsd: costCompletionChat.cost,
|
|
140
|
+
...(typeof metaChat.cost === 'number'
|
|
141
|
+
? { cost: metaChat.cost }
|
|
142
|
+
: { cost: costCompletionChat.cost })
|
|
127
143
|
}
|
|
128
|
-
: {})
|
|
144
|
+
: {}),
|
|
145
|
+
...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {})
|
|
129
146
|
}
|
|
130
147
|
};
|
|
131
148
|
// Track activity success if activity was started
|
|
132
149
|
if (activity) {
|
|
133
150
|
try {
|
|
134
151
|
await this.activityManager.logSuccess(activity, {
|
|
135
|
-
...
|
|
152
|
+
...costCompletionChat,
|
|
136
153
|
response: enhancedResponse,
|
|
137
154
|
endTime: Date.now(),
|
|
138
155
|
duration: Date.now() - startTime
|
|
@@ -245,7 +262,11 @@ export class AIGateway {
|
|
|
245
262
|
// Attach parsedSnapshot to request for activity tracking
|
|
246
263
|
request._parsedRequest = parsedSnapshot;
|
|
247
264
|
// Merge config (modelConfig > request.config > gateway defaults)
|
|
248
|
-
const
|
|
265
|
+
const aiTools = await this.getAiTools();
|
|
266
|
+
const mergedConfig = await mergeConfig(request, this.config, this.logger, {
|
|
267
|
+
defaultModelConfig: this.defaultModelConfig,
|
|
268
|
+
catalog: aiTools?.catalog ?? null
|
|
269
|
+
});
|
|
249
270
|
request._mergedRouterConfig = mergedConfig;
|
|
250
271
|
const diagnosticsMode = request.diagnostics?.mode;
|
|
251
272
|
const traceEnabled = diagnosticsMode === 'trace';
|
|
@@ -523,6 +544,8 @@ export class AIGateway {
|
|
|
523
544
|
}
|
|
524
545
|
contentType = 'structured';
|
|
525
546
|
parsingMethod = 'flex-md';
|
|
547
|
+
const outputContractKeys = resolveOutputContractFieldKeys(request);
|
|
548
|
+
parsedContent = await enrichParsedContentForOutputContract(parsedContent, content, outputContractKeys, this.logger);
|
|
526
549
|
let tokens = extractTokenUsageFromRouterResponse(routerResponse);
|
|
527
550
|
if (!(tokens.prompt || tokens.completion || tokens.total)) {
|
|
528
551
|
const alt = routerResponse?.rawResponse ?? routerResponse?.raw;
|
|
@@ -532,7 +555,11 @@ export class AIGateway {
|
|
|
532
555
|
tokens = second;
|
|
533
556
|
}
|
|
534
557
|
}
|
|
535
|
-
const
|
|
558
|
+
const costCompletion = await resolveCostCompletionWithAiTools(routerResponse, tokens, {
|
|
559
|
+
mergedConfig,
|
|
560
|
+
calculator: aiTools?.calculator ?? null,
|
|
561
|
+
calculateCost: this.config.aiTools?.calculateCost
|
|
562
|
+
});
|
|
536
563
|
const routerMetaForCost = routerResponse?.metadata || {};
|
|
537
564
|
const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
|
|
538
565
|
const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
|
|
@@ -551,14 +578,15 @@ export class AIGateway {
|
|
|
551
578
|
parsingMethod,
|
|
552
579
|
...routingMetadataSlice,
|
|
553
580
|
...(effectiveModelConfig !== undefined ? { effectiveModelConfig } : {}),
|
|
554
|
-
...(
|
|
581
|
+
...(costCompletion.costStatus === 'priced'
|
|
555
582
|
? {
|
|
556
|
-
costUsd:
|
|
583
|
+
costUsd: costCompletion.cost,
|
|
557
584
|
...(typeof routerMetaForCost.cost === 'number'
|
|
558
585
|
? { cost: routerMetaForCost.cost }
|
|
559
|
-
: { cost:
|
|
586
|
+
: { cost: costCompletion.cost })
|
|
560
587
|
}
|
|
561
588
|
: {}),
|
|
589
|
+
...(costCompletion.costStatus ? { costStatus: costCompletion.costStatus } : {}),
|
|
562
590
|
...(traceEnabled
|
|
563
591
|
? {
|
|
564
592
|
requestIds: traceRequestIds,
|
|
@@ -597,7 +625,7 @@ export class AIGateway {
|
|
|
597
625
|
usage: tokens
|
|
598
626
|
};
|
|
599
627
|
await this.activityManager.logSuccess(activity, {
|
|
600
|
-
...
|
|
628
|
+
...costCompletion,
|
|
601
629
|
response: activityResponse,
|
|
602
630
|
endTime: Date.now(),
|
|
603
631
|
duration: Date.now() - startTime
|
|
@@ -699,6 +727,10 @@ export class AIGateway {
|
|
|
699
727
|
logger: this.logger
|
|
700
728
|
});
|
|
701
729
|
}
|
|
730
|
+
getAiTools() {
|
|
731
|
+
this._aiToolsClient ??= getAiToolsClient(this.config, this.logger);
|
|
732
|
+
return this._aiToolsClient;
|
|
733
|
+
}
|
|
702
734
|
}
|
|
703
735
|
function resolveRuntimeJobId(request) {
|
|
704
736
|
return request.identity.jobId || request.identity.sessionId || request.aiRequestId;
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,12 @@ export { AIGateway } from './gateway.js';
|
|
|
17
17
|
export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
|
|
18
18
|
export { autoRegisterProviders } from './gateway-provider-auto-register.js';
|
|
19
19
|
export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions } from './types.js';
|
|
20
|
-
export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
|
|
20
|
+
export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, hasNonZeroTokenUsage } from './gateway-utils.js';
|
|
21
|
+
export { getGatewayOperationalMode, isProdGatewayMode, resolveGatewayDefaultModel, parseModelProviderSpec, CODE_DEFAULT_MODEL } from './gateway-mode.js';
|
|
22
|
+
export type { GatewayOperationalMode, GatewayDefaultModelSource, DefaultModelSubstitutionReason, ResolvedGatewayDefault } from './gateway-mode.js';
|
|
23
|
+
export type { ActivityCostStatus, ResolvedActivityCost } from './gateway-utils.js';
|
|
24
|
+
export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
|
|
25
|
+
export type { OutputContractSpec } from './output-contract-normalizer.js';
|
|
21
26
|
export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
|
|
22
27
|
export type { GatewayTemplateRenderRequestSlice } from './template-render-merge.js';
|
|
23
28
|
export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,9 @@ export * from '@x12i/ai-providers-router';
|
|
|
17
17
|
export { AIGateway } from './gateway.js';
|
|
18
18
|
export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
|
|
19
19
|
export { autoRegisterProviders } from './gateway-provider-auto-register.js';
|
|
20
|
-
export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
|
|
20
|
+
export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, hasNonZeroTokenUsage } from './gateway-utils.js';
|
|
21
|
+
export { getGatewayOperationalMode, isProdGatewayMode, resolveGatewayDefaultModel, parseModelProviderSpec, CODE_DEFAULT_MODEL } from './gateway-mode.js';
|
|
22
|
+
export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
|
|
21
23
|
export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
|
|
22
24
|
export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
|
|
23
25
|
// Usage tracking: UsageTracker class methods are available but consumption calculation is disabled
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
|
|
3
|
+
* Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
|
|
4
|
+
*/
|
|
5
|
+
import type { Logxer } from '@x12i/logxer';
|
|
6
|
+
/** Explicit contract: field names or JSON-schema-style `properties` only. */
|
|
7
|
+
export type OutputContractSpec = string[] | {
|
|
8
|
+
properties: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
|
|
12
|
+
*/
|
|
13
|
+
export declare function contractSpecToFieldKeys(contract: unknown): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveOutputContractFieldKeys(request: unknown): string[] | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
|
|
20
|
+
*/
|
|
21
|
+
export declare function enrichParsedContentForOutputContract(parsed: unknown, rawContent: string, contractKeys: string[] | undefined, logger?: Logxer): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
|
|
3
|
+
* Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
|
|
4
|
+
*/
|
|
5
|
+
import { parseMarkdownSectionsFromContent } from './flex-md-loader.js';
|
|
6
|
+
import { coalesceMergedInputBucket, extractCallerInputsBag, parseLooseJsonObject } from './memory-path-resolution.js';
|
|
7
|
+
function isPlainObject(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function hasMeaningfulContractValue(value) {
|
|
11
|
+
if (value === undefined || value === null)
|
|
12
|
+
return false;
|
|
13
|
+
if (typeof value === 'string')
|
|
14
|
+
return value.trim().length > 0;
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value.length > 0;
|
|
17
|
+
if (typeof value === 'object')
|
|
18
|
+
return Object.keys(value).length > 0;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
|
|
23
|
+
*/
|
|
24
|
+
export function contractSpecToFieldKeys(contract) {
|
|
25
|
+
if (contract == null)
|
|
26
|
+
return [];
|
|
27
|
+
if (Array.isArray(contract)) {
|
|
28
|
+
return contract.filter((k) => typeof k === 'string' && k.trim().length > 0);
|
|
29
|
+
}
|
|
30
|
+
if (!isPlainObject(contract))
|
|
31
|
+
return [];
|
|
32
|
+
const properties = contract.properties;
|
|
33
|
+
if (isPlainObject(properties)) {
|
|
34
|
+
return Object.keys(properties);
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
/** Graph-engine path: `workingMemory.inputs.outputContract` or merged `input.outputContract`. */
|
|
39
|
+
function readExplicitOutputContractFromWorkingMemory(workingMemory) {
|
|
40
|
+
if (!isPlainObject(workingMemory))
|
|
41
|
+
return undefined;
|
|
42
|
+
const inputs = extractCallerInputsBag(workingMemory);
|
|
43
|
+
if (inputs?.outputContract !== undefined)
|
|
44
|
+
return inputs.outputContract;
|
|
45
|
+
const input = coalesceMergedInputBucket(workingMemory);
|
|
46
|
+
if (isPlainObject(input) && input.outputContract !== undefined) {
|
|
47
|
+
return input.outputContract;
|
|
48
|
+
}
|
|
49
|
+
const loose = parseLooseJsonObject(workingMemory.input);
|
|
50
|
+
if (loose?.outputContract !== undefined)
|
|
51
|
+
return loose.outputContract;
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveOutputContractFieldKeys(request) {
|
|
58
|
+
if (request == null || typeof request !== 'object')
|
|
59
|
+
return undefined;
|
|
60
|
+
const r = request;
|
|
61
|
+
const candidates = [
|
|
62
|
+
r.outputContract,
|
|
63
|
+
readExplicitOutputContractFromWorkingMemory(r.workingMemory)
|
|
64
|
+
];
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
const keys = contractSpecToFieldKeys(candidate);
|
|
67
|
+
if (keys.length > 0)
|
|
68
|
+
return keys;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function asParsedRecord(parsed) {
|
|
73
|
+
if (!isPlainObject(parsed))
|
|
74
|
+
return {};
|
|
75
|
+
return { ...parsed };
|
|
76
|
+
}
|
|
77
|
+
function pickAliasValue(source, key) {
|
|
78
|
+
if (hasMeaningfulContractValue(source[key]))
|
|
79
|
+
return source[key];
|
|
80
|
+
const spaced = key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
|
|
81
|
+
const title = spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
82
|
+
const aliases = [
|
|
83
|
+
key,
|
|
84
|
+
key.toLowerCase(),
|
|
85
|
+
title,
|
|
86
|
+
title.replace(/\s+/g, ' '),
|
|
87
|
+
key.replace(/([A-Z])/g, '_$1').toLowerCase()
|
|
88
|
+
];
|
|
89
|
+
for (const alias of aliases) {
|
|
90
|
+
if (hasMeaningfulContractValue(source[alias]))
|
|
91
|
+
return source[alias];
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
|
|
97
|
+
*/
|
|
98
|
+
export async function enrichParsedContentForOutputContract(parsed, rawContent, contractKeys, logger) {
|
|
99
|
+
const base = asParsedRecord(parsed);
|
|
100
|
+
if (!contractKeys?.length)
|
|
101
|
+
return base;
|
|
102
|
+
const missing = contractKeys.filter((k) => !hasMeaningfulContractValue(base[k]));
|
|
103
|
+
if (missing.length === 0)
|
|
104
|
+
return base;
|
|
105
|
+
const content = typeof rawContent === 'string' && rawContent.trim().length > 0
|
|
106
|
+
? rawContent
|
|
107
|
+
: typeof base.rawText === 'string'
|
|
108
|
+
? base.rawText
|
|
109
|
+
: '';
|
|
110
|
+
if (!content.trim())
|
|
111
|
+
return base;
|
|
112
|
+
const fromMarkdown = parseMarkdownSectionsFromContent(content, logger);
|
|
113
|
+
const merged = { ...base };
|
|
114
|
+
for (const key of missing) {
|
|
115
|
+
const value = pickAliasValue(fromMarkdown, key);
|
|
116
|
+
if (hasMeaningfulContractValue(value)) {
|
|
117
|
+
merged[key] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return merged;
|
|
121
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -340,6 +340,27 @@ export interface GatewayConfig extends Omit<RouterConfig, 'defaultEngine' | 'log
|
|
|
340
340
|
openRouter?: {
|
|
341
341
|
enabled?: boolean;
|
|
342
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* Operational mode override (`process.env.mode` / `MODE` when omitted).
|
|
345
|
+
* - `prod`: unresolved models fall back to {@link AI_GATEWAY_DEFAULT_MODEL} / packaged default (with Logxer warn).
|
|
346
|
+
* - `dev` / `debug`: unresolved models throw {@link ModelResolutionError} from `@x12i/ai-tools`.
|
|
347
|
+
*/
|
|
348
|
+
mode?: 'dev' | 'debug' | 'prod';
|
|
349
|
+
/**
|
|
350
|
+
* @x12i/ai-tools integration: catalog model resolution (request) and cost calculation (response).
|
|
351
|
+
*/
|
|
352
|
+
aiTools?: {
|
|
353
|
+
/** @default true */
|
|
354
|
+
enabled?: boolean;
|
|
355
|
+
/** Inject Catalox; otherwise `createCataloxFromEnv()` from `@x12i/catalox/firebase`. */
|
|
356
|
+
catalox?: import('@x12i/catalox').Catalox;
|
|
357
|
+
cacheTtlMs?: number;
|
|
358
|
+
/** @default true */
|
|
359
|
+
resolveModels?: boolean;
|
|
360
|
+
/** @default true */
|
|
361
|
+
calculateCost?: boolean;
|
|
362
|
+
costIncludeBreakdown?: boolean;
|
|
363
|
+
};
|
|
343
364
|
/**
|
|
344
365
|
* InstructionsBlocks overrides
|
|
345
366
|
* Key: block name, Value: block content
|
|
@@ -701,6 +722,14 @@ interface BaseLLMRequest extends Omit<LLMRequest, 'messages' | 'input' | 'reques
|
|
|
701
722
|
* attach heavy diagnostic objects or raw provider payloads.
|
|
702
723
|
*/
|
|
703
724
|
diagnostics?: DiagnosticsOptions;
|
|
725
|
+
/**
|
|
726
|
+
* Explicit output contract from graph-engine / ai-tasks: field names or `{ properties }`.
|
|
727
|
+
* Also accepted on `workingMemory.inputs.outputContract`. When absent, the gateway does not
|
|
728
|
+
* infer contract fields from other request shapes (`expectedSchema`, config, etc.).
|
|
729
|
+
*/
|
|
730
|
+
outputContract?: string[] | {
|
|
731
|
+
properties: Record<string, unknown>;
|
|
732
|
+
};
|
|
704
733
|
}
|
|
705
734
|
/**
|
|
706
735
|
* Chat request for conversational use cases
|
|
@@ -932,6 +961,12 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
|
|
|
932
961
|
* Cost in USD (if available)
|
|
933
962
|
*/
|
|
934
963
|
cost?: number;
|
|
964
|
+
/**
|
|
965
|
+
* Billing state for Run Analysis / Activix when usage is recorded.
|
|
966
|
+
* - `priced`: {@link cost} / {@link costUsd} is a finite number from the router
|
|
967
|
+
* - `unpriced`: usage exists but no price table / adapter cost was returned
|
|
968
|
+
*/
|
|
969
|
+
costStatus?: 'priced' | 'unpriced';
|
|
935
970
|
/**
|
|
936
971
|
* Cost in USD (preferred stable key when the router exposes it).
|
|
937
972
|
* When both are present, costUsd should mirror cost.
|