@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/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
|
package/dist/activity-manager.js
CHANGED
|
@@ -155,6 +155,12 @@ function pickActivixCompletionRoutingMetadata(response) {
|
|
|
155
155
|
if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
|
|
156
156
|
out.effectiveModelConfig = m.effectiveModelConfig;
|
|
157
157
|
}
|
|
158
|
+
if (typeof m.cost === 'number' && Number.isFinite(m.cost))
|
|
159
|
+
out.cost = m.cost;
|
|
160
|
+
if (typeof m.costUsd === 'number' && Number.isFinite(m.costUsd))
|
|
161
|
+
out.costUsd = m.costUsd;
|
|
162
|
+
if (m.costStatus === 'priced' || m.costStatus === 'unpriced')
|
|
163
|
+
out.costStatus = m.costStatus;
|
|
158
164
|
return out;
|
|
159
165
|
}
|
|
160
166
|
function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
|
|
@@ -844,6 +850,7 @@ export class ActivityManager {
|
|
|
844
850
|
}
|
|
845
851
|
await this.activix.completeRecord(activity.activityId, {
|
|
846
852
|
cost: details.cost,
|
|
853
|
+
...(details.costStatus ? { costStatus: details.costStatus } : {}),
|
|
847
854
|
response: details.response,
|
|
848
855
|
outer: {
|
|
849
856
|
output: details.response,
|
|
@@ -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/flex-md-loader.d.ts
CHANGED
|
@@ -43,6 +43,11 @@ export declare function extractJsonFromFlexMd(content: string, logger?: Logxer):
|
|
|
43
43
|
json: any;
|
|
44
44
|
method: string;
|
|
45
45
|
} | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Section-based markdown → camelCase field map (e.g. `### Short Answer` → `shortAnswer`).
|
|
48
|
+
* Used when output contracts need structured `parsed` fields but flex-md returned only `rawText`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseMarkdownSectionsFromContent(content: string, logger?: Logxer): Record<string, unknown>;
|
|
46
51
|
/**
|
|
47
52
|
* Check if flex-md module is available
|
|
48
53
|
*/
|
package/dist/flex-md-loader.js
CHANGED
|
@@ -503,6 +503,22 @@ function fallbackMarkdownParser(content, logger) {
|
|
|
503
503
|
method: 'fallback-raw-text'
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* Section-based markdown → camelCase field map (e.g. `### Short Answer` → `shortAnswer`).
|
|
508
|
+
* Used when output contracts need structured `parsed` fields but flex-md returned only `rawText`.
|
|
509
|
+
*/
|
|
510
|
+
export function parseMarkdownSectionsFromContent(content, logger) {
|
|
511
|
+
const parsed = fallbackMarkdownParser(content, logger);
|
|
512
|
+
const json = parsed.json;
|
|
513
|
+
if (json != null && typeof json === 'object' && !Array.isArray(json)) {
|
|
514
|
+
const keys = Object.keys(json);
|
|
515
|
+
if (keys.length === 1 && keys[0] === 'rawText') {
|
|
516
|
+
return {};
|
|
517
|
+
}
|
|
518
|
+
return json;
|
|
519
|
+
}
|
|
520
|
+
return {};
|
|
521
|
+
}
|
|
506
522
|
/**
|
|
507
523
|
* Fallback JSON extraction when flex-md is not available
|
|
508
524
|
*/
|
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).
|
|
@@ -43,6 +48,57 @@ export declare function extractTokenUsageFromRouterResponse(routerResponse: unkn
|
|
|
43
48
|
* Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
|
|
44
49
|
*/
|
|
45
50
|
export declare function extractCostUsdFromRouterResponse(routerResponse: unknown): number | undefined;
|
|
51
|
+
/** Activity billing state when token usage is recorded (Run Analysis G8). */
|
|
52
|
+
export type ActivityCostStatus = 'priced' | 'unpriced';
|
|
53
|
+
export type ResolvedActivityCost = {
|
|
54
|
+
cost?: number;
|
|
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
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export declare function hasNonZeroTokenUsage(tokens: {
|
|
67
|
+
prompt: number;
|
|
68
|
+
completion: number;
|
|
69
|
+
total: number;
|
|
70
|
+
}): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Gateway fallback when the router does not set `metadata.costStatus`.
|
|
73
|
+
* Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveActivityCostCompletion(tokens: {
|
|
76
|
+
prompt: number;
|
|
77
|
+
completion: number;
|
|
78
|
+
total: number;
|
|
79
|
+
}, costUsd: number | undefined): ResolvedActivityCost;
|
|
80
|
+
/**
|
|
81
|
+
* Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
|
|
82
|
+
* otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveCostCompletionForActivity(routerResponse: unknown, tokens: {
|
|
85
|
+
prompt: number;
|
|
86
|
+
completion: number;
|
|
87
|
+
total: number;
|
|
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>;
|
|
46
102
|
/**
|
|
47
103
|
* Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
|
|
48
104
|
* Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
|