@x12i/ai-gateway 9.2.0 → 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/ai-tools-client.d.ts +20 -0
- package/dist/ai-tools-client.js +91 -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 +28 -1
- package/dist/gateway-utils.js +137 -12
- package/dist/gateway.d.ts +3 -0
- package/dist/gateway.js +29 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/types.d.ts +21 -0
- package/dist-cjs/activity-manager.cjs +14 -19
- 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 +20 -67
- 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 +160 -89
- package/dist-cjs/gateway-utils.d.ts +28 -1
- package/dist-cjs/gateway-validation.cjs +2 -6
- package/dist-cjs/gateway.cjs +86 -66
- package/dist-cjs/gateway.d.ts +3 -0
- package/dist-cjs/index.cjs +22 -98
- package/dist-cjs/index.d.ts +3 -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 +9 -14
- 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 +21 -0
- package/dist-cjs/usage-tracker.cjs +3 -7
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -368,6 +368,51 @@ The gateway only exposes official queryable clients. It exposes `activixClient`
|
|
|
368
368
|
|
|
369
369
|
See [Runtime Objects Observability Methodology](./docs/RUNTIME_OBJECTS_OBSERVABILITY.md) for the reusable package-level contract.
|
|
370
370
|
|
|
371
|
+
### Model catalog resolution and defaults (`@x12i/ai-tools`)
|
|
372
|
+
|
|
373
|
+
Before each invoke, the gateway can normalize caller `config.model` / `modelConfig` via the **ai-models** Catalox catalog (`@x12i/ai-tools`). After invoke, when the router leaves cost **unpriced**, the gateway may compute USD from the same catalog.
|
|
374
|
+
|
|
375
|
+
**Environment variables:**
|
|
376
|
+
|
|
377
|
+
| Variable | Purpose |
|
|
378
|
+
|----------|---------|
|
|
379
|
+
| `AI_GATEWAY_DEFAULT_MODEL` | Default model when none is provided, or when resolution fails in **`mode=prod`**. Supports `provider/model` (e.g. `openrouter/openai/gpt-5-nano`) or a bare model id. |
|
|
380
|
+
| `mode` / `MODE` | `prod` — unresolved models fall back to the default chain (with **Logxer `warn`**). `dev` / `debug` / omitted — unresolved models throw **`ModelResolutionError`**. |
|
|
381
|
+
|
|
382
|
+
**Default model priority** (prod fallback only): `AI_GATEWAY_DEFAULT_MODEL` → `src/defaults/model-config.json` `defaultModel` → code constant `gpt-5-nano`.
|
|
383
|
+
|
|
384
|
+
**Logxer warnings** on default substitution include structured fields: `reason` (`no_model_provided`, `model_resolution_failed`, `ai_tools_unavailable`), `defaultSource` (`env`, `model-config.json`, `code`), `originalModel`, `defaultModel`, and `mode`.
|
|
385
|
+
|
|
386
|
+
Catalox/Firebase credentials are required for catalog bootstrap (same as `@x12i/ai-tools` — see that package’s README). Disable with `aiTools: { enabled: false }` on `GatewayConfig`, or inject `aiTools.catalox` for tests.
|
|
387
|
+
|
|
388
|
+
**GatewayConfig (optional overrides):**
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
const gateway = new AIGateway({
|
|
392
|
+
mode: 'prod', // or 'dev' | 'debug' — overrides process.env.mode
|
|
393
|
+
aiTools: {
|
|
394
|
+
enabled: true,
|
|
395
|
+
resolveModels: true,
|
|
396
|
+
calculateCost: true,
|
|
397
|
+
costIncludeBreakdown: false,
|
|
398
|
+
cacheTtlMs: 60_000,
|
|
399
|
+
// catalox: injectedCataloxInstance,
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Tests before release:**
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
npm run build
|
|
408
|
+
npm test # integration (tsx)
|
|
409
|
+
npm run test:ai-tools # unit: mode, defaults, cost helper
|
|
410
|
+
npm run test:live # LIVE: catalog + invoke (needs .env + Firebase + LLM key)
|
|
411
|
+
npm run test:real:comprehensive # optional: compiled real router matrix + npm test
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
See [`.env.example`](./.env.example) for `AI_GATEWAY_DEFAULT_MODEL`, `mode`, provider keys, and Firebase/Catalox variables.
|
|
415
|
+
|
|
371
416
|
**Recommended (auto-configured from environment variables):**
|
|
372
417
|
|
|
373
418
|
```typescript
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy @x12i/ai-tools catalog + cost calculator bootstrap.
|
|
3
|
+
*/
|
|
4
|
+
import { AiModelsCatalogClient, CostCalculator, type ModelResolutionSuccess } from '@x12i/ai-tools';
|
|
5
|
+
import type { Logxer } from '@x12i/logxer';
|
|
6
|
+
import type { ChatRequest, GatewayConfig } from './types.js';
|
|
7
|
+
export type AiToolsClientBundle = {
|
|
8
|
+
catalog: AiModelsCatalogClient;
|
|
9
|
+
calculator: CostCalculator;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Returns catalog + calculator, or null when disabled or bootstrap fails.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getAiToolsClient(config: GatewayConfig, logger: Logxer): Promise<AiToolsClientBundle | null>;
|
|
15
|
+
/** Reset singleton (tests). */
|
|
16
|
+
export declare function resetAiToolsClientForTests(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Map catalog resolution to router config provider/model fields.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applyModelResolution(merged: NonNullable<ChatRequest['config']>, resolution: ModelResolutionSuccess, gatewayDefaultEngine?: string): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy @x12i/ai-tools catalog + cost calculator bootstrap.
|
|
3
|
+
*/
|
|
4
|
+
import { AiModelsCatalogClient, CostCalculator, ensureAiModelsCatalog } from '@x12i/ai-tools';
|
|
5
|
+
import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
|
|
6
|
+
let sharedClientPromise = null;
|
|
7
|
+
let sharedConfigKey;
|
|
8
|
+
let bootstrapFailedLogged = false;
|
|
9
|
+
function configKey(config) {
|
|
10
|
+
const injected = config.aiTools?.catalox ? 'injected' : 'env';
|
|
11
|
+
return `${injected}:${config.aiTools?.cacheTtlMs ?? ''}:${config.aiTools?.costIncludeBreakdown ?? ''}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns catalog + calculator, or null when disabled or bootstrap fails.
|
|
15
|
+
*/
|
|
16
|
+
export async function getAiToolsClient(config, logger) {
|
|
17
|
+
if (config.aiTools?.enabled === false) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const key = configKey(config);
|
|
21
|
+
if (sharedClientPromise && sharedConfigKey !== key) {
|
|
22
|
+
sharedClientPromise = null;
|
|
23
|
+
}
|
|
24
|
+
sharedConfigKey = key;
|
|
25
|
+
if (!sharedClientPromise) {
|
|
26
|
+
sharedClientPromise = bootstrapAiTools(config, logger);
|
|
27
|
+
}
|
|
28
|
+
return sharedClientPromise;
|
|
29
|
+
}
|
|
30
|
+
/** Reset singleton (tests). */
|
|
31
|
+
export function resetAiToolsClientForTests() {
|
|
32
|
+
sharedClientPromise = null;
|
|
33
|
+
sharedConfigKey = undefined;
|
|
34
|
+
bootstrapFailedLogged = false;
|
|
35
|
+
}
|
|
36
|
+
async function bootstrapAiTools(config, logger) {
|
|
37
|
+
try {
|
|
38
|
+
let catalox = config.aiTools?.catalox;
|
|
39
|
+
if (!catalox) {
|
|
40
|
+
const { createCataloxFromEnv } = await import('@x12i/catalox/firebase');
|
|
41
|
+
const bootstrapped = createCataloxFromEnv();
|
|
42
|
+
catalox = bootstrapped.catalox;
|
|
43
|
+
}
|
|
44
|
+
await ensureAiModelsCatalog(catalox);
|
|
45
|
+
const catalog = new AiModelsCatalogClient({
|
|
46
|
+
catalox,
|
|
47
|
+
cacheTtlMs: config.aiTools?.cacheTtlMs
|
|
48
|
+
});
|
|
49
|
+
const calculator = new CostCalculator(catalog, {
|
|
50
|
+
includeBreakdown: config.aiTools?.costIncludeBreakdown === true
|
|
51
|
+
});
|
|
52
|
+
logger.debug('ai-tools catalog client ready', {
|
|
53
|
+
debugKind: gatewayLogDebug.state
|
|
54
|
+
});
|
|
55
|
+
return { catalog, calculator };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (!bootstrapFailedLogged) {
|
|
59
|
+
bootstrapFailedLogged = true;
|
|
60
|
+
logger.warn('ai-tools catalog bootstrap failed; model resolution and catalog cost calculation disabled', withActivityIdentity(undefined, {
|
|
61
|
+
error: error instanceof Error ? error.message : String(error),
|
|
62
|
+
debugKind: gatewayLogDebug.anomaly
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Map catalog resolution to router config provider/model fields.
|
|
70
|
+
*/
|
|
71
|
+
export function applyModelResolution(merged, resolution, gatewayDefaultEngine) {
|
|
72
|
+
if (resolution.routedViaOpenRouter) {
|
|
73
|
+
merged.provider = 'openrouter';
|
|
74
|
+
merged.model = resolution.modelId;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const slash = resolution.modelId.indexOf('/');
|
|
78
|
+
if (slash > 0) {
|
|
79
|
+
merged.provider = resolution.record?.providerId ?? resolution.modelId.slice(0, slash);
|
|
80
|
+
merged.model = resolution.modelId.slice(slash + 1);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
merged.model = resolution.modelId;
|
|
84
|
+
if (resolution.record?.providerId) {
|
|
85
|
+
merged.provider = resolution.record.providerId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!merged.provider && gatewayDefaultEngine) {
|
|
89
|
+
merged.provider = gatewayDefaultEngine;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/gateway-config.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface GatewayConfigContext {
|
|
|
19
19
|
usageTracker: UsageTracker;
|
|
20
20
|
messageBuilderConfig: MessageBuilderConfig;
|
|
21
21
|
}
|
|
22
|
+
export type InitializedGatewayComponents = ReturnType<typeof initializeGatewayComponents>;
|
|
22
23
|
/**
|
|
23
24
|
* Loads configuration from JSON files (model config and instructionsBlocks).
|
|
24
25
|
* Pass a {@link Logxer} instance so load diagnostics go through logxer (not console).
|
|
@@ -46,4 +47,5 @@ export declare function initializeGatewayComponents(config: GatewayConfig): {
|
|
|
46
47
|
activityManager: ActivityManager;
|
|
47
48
|
usageTracker: UsageTracker;
|
|
48
49
|
messageBuilderConfig: MessageBuilderConfig;
|
|
50
|
+
defaultModelConfig: Record<string, unknown>;
|
|
49
51
|
};
|
package/dist/gateway-config.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway operational mode (prod vs dev/debug) and default model resolution.
|
|
3
|
+
*/
|
|
4
|
+
import type { Logxer } from '@x12i/logxer';
|
|
5
|
+
import type { ActivityIdentity, GatewayConfig } from './types.js';
|
|
6
|
+
export type GatewayOperationalMode = 'prod' | 'debug' | 'dev';
|
|
7
|
+
export type GatewayDefaultModelSource = 'env' | 'model-config.json' | 'code';
|
|
8
|
+
export type DefaultModelSubstitutionReason = 'no_model_provided' | 'model_resolution_failed' | 'ai_tools_unavailable';
|
|
9
|
+
export declare const CODE_DEFAULT_MODEL = "gpt-5-nano";
|
|
10
|
+
export type ResolvedGatewayDefault = {
|
|
11
|
+
model: string;
|
|
12
|
+
provider?: string;
|
|
13
|
+
source: GatewayDefaultModelSource;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Operational mode: `GatewayConfig.mode` overrides `process.env.mode` / `MODE`.
|
|
17
|
+
* Only `prod` allows silent default-model substitution; all other values are strict.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getGatewayOperationalMode(config?: Pick<GatewayConfig, 'mode'>): GatewayOperationalMode;
|
|
20
|
+
export declare function isProdGatewayMode(mode: GatewayOperationalMode): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Parse `provider/model` or bare model id (OpenRouter ids may contain multiple slashes).
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseModelProviderSpec(spec: string): {
|
|
25
|
+
provider?: string;
|
|
26
|
+
model: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Default model priority: AI_GATEWAY_DEFAULT_MODEL → model-config.json → code constant.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveGatewayDefaultModel(defaultModelConfig?: Record<string, unknown>, gatewayDefaultEngine?: string): ResolvedGatewayDefault;
|
|
32
|
+
export declare function warnDefaultModelSubstitution(logger: Logxer, identity: Partial<ActivityIdentity> | undefined, details: {
|
|
33
|
+
reason: DefaultModelSubstitutionReason;
|
|
34
|
+
mode: GatewayOperationalMode;
|
|
35
|
+
defaultSource: GatewayDefaultModelSource;
|
|
36
|
+
defaultProvider?: string;
|
|
37
|
+
defaultModel: string;
|
|
38
|
+
originalProvider?: string;
|
|
39
|
+
originalModel?: string;
|
|
40
|
+
}): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway operational mode (prod vs dev/debug) and default model resolution.
|
|
3
|
+
*/
|
|
4
|
+
import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
|
|
5
|
+
export const CODE_DEFAULT_MODEL = 'gpt-5-nano';
|
|
6
|
+
/**
|
|
7
|
+
* Operational mode: `GatewayConfig.mode` overrides `process.env.mode` / `MODE`.
|
|
8
|
+
* Only `prod` allows silent default-model substitution; all other values are strict.
|
|
9
|
+
*/
|
|
10
|
+
export function getGatewayOperationalMode(config) {
|
|
11
|
+
if (config?.mode) {
|
|
12
|
+
return config.mode;
|
|
13
|
+
}
|
|
14
|
+
const raw = (process.env.mode ?? process.env.MODE ?? '').toLowerCase();
|
|
15
|
+
if (raw === 'prod')
|
|
16
|
+
return 'prod';
|
|
17
|
+
if (raw === 'dev')
|
|
18
|
+
return 'dev';
|
|
19
|
+
return 'debug';
|
|
20
|
+
}
|
|
21
|
+
export function isProdGatewayMode(mode) {
|
|
22
|
+
return mode === 'prod';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse `provider/model` or bare model id (OpenRouter ids may contain multiple slashes).
|
|
26
|
+
*/
|
|
27
|
+
export function parseModelProviderSpec(spec) {
|
|
28
|
+
const trimmed = spec.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
return { model: CODE_DEFAULT_MODEL };
|
|
31
|
+
}
|
|
32
|
+
const slash = trimmed.indexOf('/');
|
|
33
|
+
if (slash === -1) {
|
|
34
|
+
return { model: trimmed };
|
|
35
|
+
}
|
|
36
|
+
const first = trimmed.slice(0, slash);
|
|
37
|
+
const rest = trimmed.slice(slash + 1);
|
|
38
|
+
if (rest.includes('/') && (first === 'openrouter' || first === 'open-router')) {
|
|
39
|
+
return { provider: 'openrouter', model: trimmed };
|
|
40
|
+
}
|
|
41
|
+
return { provider: first, model: rest };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Default model priority: AI_GATEWAY_DEFAULT_MODEL → model-config.json → code constant.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveGatewayDefaultModel(defaultModelConfig, gatewayDefaultEngine) {
|
|
47
|
+
const envSpec = process.env.AI_GATEWAY_DEFAULT_MODEL?.trim();
|
|
48
|
+
if (envSpec) {
|
|
49
|
+
const parsed = parseModelProviderSpec(envSpec);
|
|
50
|
+
return { model: parsed.model, provider: parsed.provider, source: 'env' };
|
|
51
|
+
}
|
|
52
|
+
const jsonModel = typeof defaultModelConfig?.defaultModel === 'string' ? defaultModelConfig.defaultModel : undefined;
|
|
53
|
+
if (jsonModel) {
|
|
54
|
+
const parsed = parseModelProviderSpec(jsonModel);
|
|
55
|
+
const jsonEngine = typeof defaultModelConfig?.defaultEngine === 'string'
|
|
56
|
+
? defaultModelConfig.defaultEngine
|
|
57
|
+
: gatewayDefaultEngine;
|
|
58
|
+
return {
|
|
59
|
+
model: parsed.model,
|
|
60
|
+
provider: parsed.provider ?? jsonEngine,
|
|
61
|
+
source: 'model-config.json'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
model: CODE_DEFAULT_MODEL,
|
|
66
|
+
provider: gatewayDefaultEngine,
|
|
67
|
+
source: 'code'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function warnDefaultModelSubstitution(logger, identity, details) {
|
|
71
|
+
logger.warn('Gateway substituted default model for request', withActivityIdentity(identity, {
|
|
72
|
+
...details,
|
|
73
|
+
debugKind: gatewayLogDebug.anomaly
|
|
74
|
+
}));
|
|
75
|
+
}
|
package/dist/gateway-utils.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AIInvokeRequest, ChatRequest, GatewayConfig, GatewayInvokeRejectionMetadata, GatewayTraceMergedConfig, GatewayTraceRequestIds, ModelConfig } from './types.js';
|
|
6
6
|
import type { Logxer } from '@x12i/logxer';
|
|
7
|
+
import { type AiModelsCatalogClient, type CostCalculator } from '@x12i/ai-tools';
|
|
7
8
|
/**
|
|
8
9
|
* Generates MD5 hash of a string
|
|
9
10
|
*/
|
|
@@ -12,13 +13,17 @@ export declare function generateMD5Hash(text: string): string;
|
|
|
12
13
|
* Auto-generates taskTypeId from MD5 hash of pre-parsed instructions if not provided
|
|
13
14
|
*/
|
|
14
15
|
export declare function ensureTaskTypeId(request: ChatRequest, logger: Logxer): Promise<string>;
|
|
16
|
+
export type MergeConfigOptions = {
|
|
17
|
+
defaultModelConfig?: Record<string, unknown>;
|
|
18
|
+
catalog?: AiModelsCatalogClient | null;
|
|
19
|
+
};
|
|
15
20
|
/**
|
|
16
21
|
* Merges config with defaults
|
|
17
22
|
* Supports using internal system action defaults (internalSkill or skillAudit) when useInternalDefaults is set
|
|
18
23
|
*/
|
|
19
24
|
export declare function mergeConfig(request: ChatRequest & {
|
|
20
25
|
useInternalDefaults?: 'skill' | 'audit';
|
|
21
|
-
}, config: GatewayConfig, logger: Logxer): Promise<ChatRequest['config']>;
|
|
26
|
+
}, config: GatewayConfig, logger: Logxer, mergeOptions?: MergeConfigOptions): Promise<ChatRequest['config']>;
|
|
22
27
|
/**
|
|
23
28
|
* Maps provider/router usage objects to gateway token counts (`metadata.tokens`, Activix, trace attempts).
|
|
24
29
|
* Handles promptTokens/inputTokens, OpenAI-style snake_case, Responses-style input/output tokens, and missing total (sum prompt+completion).
|
|
@@ -48,6 +53,15 @@ export type ActivityCostStatus = 'priced' | 'unpriced';
|
|
|
48
53
|
export type ResolvedActivityCost = {
|
|
49
54
|
cost?: number;
|
|
50
55
|
costStatus?: ActivityCostStatus;
|
|
56
|
+
costBreakdown?: {
|
|
57
|
+
promptCostUsd: number;
|
|
58
|
+
completionCostUsd: number;
|
|
59
|
+
cachingCostUsd?: number;
|
|
60
|
+
reasoningCostUsd?: number;
|
|
61
|
+
audioCostUsd?: number;
|
|
62
|
+
imageCostUsd?: number;
|
|
63
|
+
requestFlatCostUsd?: number;
|
|
64
|
+
};
|
|
51
65
|
};
|
|
52
66
|
export declare function hasNonZeroTokenUsage(tokens: {
|
|
53
67
|
prompt: number;
|
|
@@ -72,6 +86,19 @@ export declare function resolveCostCompletionForActivity(routerResponse: unknown
|
|
|
72
86
|
completion: number;
|
|
73
87
|
total: number;
|
|
74
88
|
}): ResolvedActivityCost;
|
|
89
|
+
export type ResolveCostCompletionOptions = {
|
|
90
|
+
mergedConfig?: unknown;
|
|
91
|
+
calculator?: CostCalculator | null;
|
|
92
|
+
calculateCost?: boolean;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Router cost passthrough, then optional @x12i/ai-tools catalog pricing when still unpriced.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveCostCompletionWithAiTools(routerResponse: unknown, tokens: {
|
|
98
|
+
prompt: number;
|
|
99
|
+
completion: number;
|
|
100
|
+
total: number;
|
|
101
|
+
}, options?: ResolveCostCompletionOptions): Promise<ResolvedActivityCost>;
|
|
75
102
|
/**
|
|
76
103
|
* Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
|
|
77
104
|
* Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
|
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
|
|
@@ -359,6 +434,56 @@ export function resolveCostCompletionForActivity(routerResponse, tokens) {
|
|
|
359
434
|
}
|
|
360
435
|
return resolveActivityCostCompletion(tokens, costUsd);
|
|
361
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
|
+
}
|
|
362
487
|
/**
|
|
363
488
|
* Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
|
|
364
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
|
@@ -9,7 +9,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
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,
|
|
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';
|
|
13
14
|
import { autoRegisterProviders } from './gateway-provider-auto-register.js';
|
|
14
15
|
import { setGatewayLastJobId, setGatewayRuntimeClients } from './runtime-objects.js';
|
|
15
16
|
import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
|
|
@@ -45,7 +46,9 @@ export class AIGateway {
|
|
|
45
46
|
logger;
|
|
46
47
|
activityManager;
|
|
47
48
|
messageBuilderConfig;
|
|
49
|
+
defaultModelConfig = {};
|
|
48
50
|
_autoRegisterDone = false;
|
|
51
|
+
_aiToolsClient = null;
|
|
49
52
|
constructor(config = {}, activityManager) {
|
|
50
53
|
this.config = config;
|
|
51
54
|
this.activityManager = activityManager;
|
|
@@ -54,6 +57,7 @@ export class AIGateway {
|
|
|
54
57
|
this.router = components.router;
|
|
55
58
|
this.activityManager = components.activityManager;
|
|
56
59
|
this.messageBuilderConfig = components.messageBuilderConfig;
|
|
60
|
+
this.defaultModelConfig = components.defaultModelConfig ?? {};
|
|
57
61
|
setGatewayRuntimeClients({
|
|
58
62
|
activix: this.activityManager?.getTracker(),
|
|
59
63
|
logger: this.logger
|
|
@@ -77,7 +81,11 @@ export class AIGateway {
|
|
|
77
81
|
// Simple message construction
|
|
78
82
|
const messages = this.buildSimpleMessages(request);
|
|
79
83
|
// Merge config (modelConfig > request.config > gateway defaults)
|
|
80
|
-
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
|
+
});
|
|
81
89
|
// Activix start snapshot must match what the router receives (modelConfig-only callers omit request.config.model).
|
|
82
90
|
request._mergedRouterConfig = mergedConfig;
|
|
83
91
|
// Lazy auto-register providers from env (OPENAI_API_KEY, etc.) so consumers don't have to call init
|
|
@@ -111,7 +119,11 @@ export class AIGateway {
|
|
|
111
119
|
});
|
|
112
120
|
const metaChat = response?.metadata || {};
|
|
113
121
|
const tokensChat = extractTokenUsageFromRouterResponse(response);
|
|
114
|
-
const costCompletionChat =
|
|
122
|
+
const costCompletionChat = await resolveCostCompletionWithAiTools(response, tokensChat, {
|
|
123
|
+
mergedConfig,
|
|
124
|
+
calculator: aiTools?.calculator ?? null,
|
|
125
|
+
calculateCost: this.config.aiTools?.calculateCost
|
|
126
|
+
});
|
|
115
127
|
// Create enhanced response
|
|
116
128
|
const enhancedResponse = {
|
|
117
129
|
content: response.content || '',
|
|
@@ -250,7 +262,11 @@ export class AIGateway {
|
|
|
250
262
|
// Attach parsedSnapshot to request for activity tracking
|
|
251
263
|
request._parsedRequest = parsedSnapshot;
|
|
252
264
|
// Merge config (modelConfig > request.config > gateway defaults)
|
|
253
|
-
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
|
+
});
|
|
254
270
|
request._mergedRouterConfig = mergedConfig;
|
|
255
271
|
const diagnosticsMode = request.diagnostics?.mode;
|
|
256
272
|
const traceEnabled = diagnosticsMode === 'trace';
|
|
@@ -539,7 +555,11 @@ export class AIGateway {
|
|
|
539
555
|
tokens = second;
|
|
540
556
|
}
|
|
541
557
|
}
|
|
542
|
-
const costCompletion =
|
|
558
|
+
const costCompletion = await resolveCostCompletionWithAiTools(routerResponse, tokens, {
|
|
559
|
+
mergedConfig,
|
|
560
|
+
calculator: aiTools?.calculator ?? null,
|
|
561
|
+
calculateCost: this.config.aiTools?.calculateCost
|
|
562
|
+
});
|
|
543
563
|
const routerMetaForCost = routerResponse?.metadata || {};
|
|
544
564
|
const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
|
|
545
565
|
const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
|
|
@@ -707,6 +727,10 @@ export class AIGateway {
|
|
|
707
727
|
logger: this.logger
|
|
708
728
|
});
|
|
709
729
|
}
|
|
730
|
+
getAiTools() {
|
|
731
|
+
this._aiToolsClient ??= getAiToolsClient(this.config, this.logger);
|
|
732
|
+
return this._aiToolsClient;
|
|
733
|
+
}
|
|
710
734
|
}
|
|
711
735
|
function resolveRuntimeJobId(request) {
|
|
712
736
|
return request.identity.jobId || request.identity.sessionId || request.aiRequestId;
|