@x12i/ai-gateway 10.1.0 → 10.2.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 +22 -8
- package/dist/gateway-utils.js +22 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/instruction-errors.d.ts +5 -0
- package/dist/instruction-errors.js +10 -0
- package/dist/invoke-model-ingress.d.ts +17 -0
- package/dist/invoke-model-ingress.js +56 -0
- package/dist-cjs/gateway-utils.cjs +22 -5
- package/dist-cjs/index.cjs +2 -1
- package/dist-cjs/index.d.ts +2 -1
- package/dist-cjs/instruction-errors.cjs +10 -0
- package/dist-cjs/instruction-errors.d.ts +5 -0
- package/dist-cjs/invoke-model-ingress.cjs +56 -0
- package/dist-cjs/invoke-model-ingress.d.ts +17 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Unified gateway for LLM provider routing, structured logging, optional Activix a
|
|
|
13
13
|
| **Activix** | Optional Mongo-backed activity rows; billing written from gateway-computed slice on **`completeRecord`** (`outer.cost` + root fields). No Activix **`autoCost`** re-pricing. |
|
|
14
14
|
| **Trace mode** | `diagnostics.mode === 'trace'` adds `metadata.attempts[]`, `metadata.usage`, and per-attempt **`costUsd`** / **`costStatus`**. |
|
|
15
15
|
|
|
16
|
-
Pinned dependency versions are in `package.json` (currently **Activix ^8.6**, **ai-tools ^3.
|
|
16
|
+
Pinned dependency versions are in `package.json` (currently **Activix ^8.6**, **ai-tools ^3.1**, **ai-profiles ^3.2**, **ai-providers-router ^4.9**).
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
@@ -87,7 +87,7 @@ console.log(response.content, response.metadata?.costUsd, response.metadata?.tok
|
|
|
87
87
|
|
|
88
88
|
### Providers without manual `register()`
|
|
89
89
|
|
|
90
|
-
- **OpenRouter:** Set **`OPENROUTER_API_KEY`** in `.env`. The gateway always passes this key to the router when set. **By default, OpenRouter is preferred** for routing (including when you also have direct keys such as `OPENAI_API_KEY`). **`@x12i/ai-tools`** resolves concrete model ids + provider via `resolveInvokeModel()` (catalog normalization, OpenRouter vs direct transport, router proxy flags). Pass catalog model ids such as `openai/gpt-4o-mini` or `
|
|
90
|
+
- **OpenRouter:** Set **`OPENROUTER_API_KEY`** in `.env`. The gateway always passes this key to the router when set. **By default, OpenRouter is preferred** for routing (including when you also have direct keys such as `OPENAI_API_KEY`). **`@x12i/ai-tools`** resolves concrete model ids + provider via `resolveInvokeModel()` (catalog normalization, OpenRouter vs direct transport, router proxy flags). Pass catalog model ids such as `openai/gpt-4o-mini` or `{ provider: 'openrouter', model: 'deepseek/deepseek-v4-pro' }`. Composite display slugs like `openrouter/deepseek/deepseek-v4-pro` are split at ingress (see **Invoke model ingress** below). Profile/choice aliases (`cheap/default`, …) must be resolved upstream (ai-tasks / `resolveAIProfile`) — the gateway rejects them with **`GATEWAY_ALIAS_MODEL_REJECTED`**.
|
|
91
91
|
- **`USE_OPENROUTER=false`:** Do **not** prefer OpenRouter when a direct provider API key exists — use the direct provider instead. OpenRouter is **still** used as **fallback** when the request targets a provider without a direct key (e.g. `anthropic` without `ANTHROPIC_API_KEY`). It does not disable OpenRouter while `OPENROUTER_API_KEY` is set.
|
|
92
92
|
- **Direct providers:** Set `OPENAI_API_KEY`, `GROK_API_KEY`, etc. Registered lazily on first invoke.
|
|
93
93
|
|
|
@@ -146,7 +146,7 @@ import {
|
|
|
146
146
|
} from '@x12i/ai-gateway';
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
**Required on every invoke:** `config.model` (or `modelConfig.model`) and `maxTokens` (`request.config`, `modelConfig`, `GatewayConfig`, or `internalSystemActions`). Missing model → `ModelRequiredError` (`code: 'MODEL_REQUIRED'`). Missing `maxTokens` → `MaxTokensRequiredError` (`code: 'MAX_TOKENS_REQUIRED'`). There is **no** packaged default model, **no** flex-md / Optimixer auto-fill, and **no** `GATEWAY_DEFAULT_MAX_TOKENS`. Use [@x12i/optimixer](https://www.npmjs.com/package/@x12i/optimixer) in the **client** that wraps this gateway if you want adaptive completion budgets.
|
|
149
|
+
**Required on every invoke:** `config.model` (or `modelConfig.model`) and `maxTokens` (`request.config`, `modelConfig`, `GatewayConfig`, or `internalSystemActions`). Missing model → `ModelRequiredError` (`code: 'MODEL_REQUIRED'`). Missing `maxTokens` → `MaxTokensRequiredError` (`code: 'MAX_TOKENS_REQUIRED'`). Profile/choice alias at invoke → `GatewayAliasModelRejectedError` (`code: 'GATEWAY_ALIAS_MODEL_REJECTED'`). There is **no** packaged default model, **no** flex-md / Optimixer auto-fill, and **no** `GATEWAY_DEFAULT_MAX_TOKENS`. Use [@x12i/optimixer](https://www.npmjs.com/package/@x12i/optimixer) in the **client** that wraps this gateway if you want adaptive completion budgets.
|
|
150
150
|
|
|
151
151
|
**Rate limiting:** removed from the gateway. See [upstream rate-limit spec](./docs/upstream-reports/AI_PROVIDER_ROUTER_RATE_LIMITING.md) — implement in `@x12i/ai-providers-router`.
|
|
152
152
|
|
|
@@ -210,7 +210,21 @@ Exports: `GATEWAY_LOGXER_PACKAGE`, `GATEWAY_LOG_ENV_PREFIX`, `createGatewayLogge
|
|
|
210
210
|
|
|
211
211
|
---
|
|
212
212
|
|
|
213
|
-
##
|
|
213
|
+
## Invoke model ingress (v10.2+)
|
|
214
|
+
|
|
215
|
+
Before catalog lookup, **`mergeConfig()`** normalizes the invoke wire shape via **`normalizeInvokeModel`** from **`@x12i/ai-profiles`** (defense in depth — idempotent when upstream already fixed):
|
|
216
|
+
|
|
217
|
+
| Input | Result |
|
|
218
|
+
|-------|--------|
|
|
219
|
+
| `model: 'openrouter/deepseek/deepseek-v4-pro'`, `provider: 'unspecified'` | `{ provider: 'openrouter', model: 'deepseek/deepseek-v4-pro' }` → catalog lookup |
|
|
220
|
+
| `{ provider: 'openrouter', model: 'deepseek/deepseek-v4-pro' }` | Pass-through unchanged |
|
|
221
|
+
| `model: 'cheap/default'` (profile/choice alias) | **`GATEWAY_ALIAS_MODEL_REJECTED`** — resolve upstream via ai-tasks / `resolveAIProfile` |
|
|
222
|
+
|
|
223
|
+
Exported helpers: `normalizeInvokeModelAtIngress`, `GatewayAliasModelRejectedError`.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## @x12i/ai-tools v3 (models + cost)
|
|
214
228
|
|
|
215
229
|
Engine-owned catalog bootstrap and post-call billing. Consumers read **`metadata.costUsd`** / **`costStatus`** only — no direct `@x12i/ai-tools` dependency for cost.
|
|
216
230
|
|
|
@@ -234,7 +248,7 @@ Implemented in **`resolveCostCompletionWithAiTools`** only ( **`CostCalculator.c
|
|
|
234
248
|
| **`enabled`** | `true` | Bootstrap **`AiModelsCatalogClient`** + **`CostCalculator`** |
|
|
235
249
|
| **`calculateCost`** | `true` | Run post-call catalog pricing when router did not price |
|
|
236
250
|
| **`resolveModels`** | `true` | **`mergeConfig()`** → **`resolveInvokeModel()`** |
|
|
237
|
-
| **`modelsOnly`** | `true` | Reject profile shortcuts (`cheapest`,
|
|
251
|
+
| **`modelsOnly`** | `true` | Reject profile shortcuts at catalog resolution (`cheapest`, …). Aliases are **always** rejected at ingress regardless of this flag. |
|
|
238
252
|
| **`bundledOnly`** | `false` | Offline bundled catalogs only |
|
|
239
253
|
| **`costIncludeBreakdown`** | `false` | Include prompt/completion breakdown on priced results |
|
|
240
254
|
| **`catalogLane`** | `"text"` (ai-tools default) | Catalog lane for resolution + cost lookup (`text`, `image`, …) |
|
|
@@ -242,7 +256,7 @@ Implemented in **`resolveCostCompletionWithAiTools`** only ( **`CostCalculator.c
|
|
|
242
256
|
|
|
243
257
|
- **No Catalox / Firestore** — catalogs come from ai-tools open-assets JSON (optional **`bundledOnly`**).
|
|
244
258
|
|
|
245
|
-
Gateway exports the model orchestrator from `@x12i/ai-tools` ≥ **3.0.0** (`resolveInvokeModel`, `useOpenRouter`, …).
|
|
259
|
+
Gateway exports the model orchestrator from `@x12i/ai-tools` ≥ **3.0.0** (`resolveInvokeModel`, `useOpenRouter`, …). Profile/choice keys (`cheap/default`, …) must be resolved **before** invoke — the gateway does not run alias resolution. Shortcuts like `cheapest` / bare `cheap` are rejected at ingress or catalog resolution.
|
|
246
260
|
|
|
247
261
|
Gateway billing helpers (exported for tests/integrators): `resolveCostCompletionWithAiTools`, `buildGatewayPricingRecord`, `catalogPricingSucceeded`, `buildTraceUsageSummary`, `enrichTraceAttemptsWithBilling`.
|
|
248
262
|
|
|
@@ -317,7 +331,7 @@ Adds **`metadata.attempts`**, **`metadata.usage`**, **`metadata.requestIds`**, a
|
|
|
317
331
|
|
|
318
332
|
Set via constructor `mode` or env `mode` / `MODE`. **Downstream hosts should document and expose `mode`** so graph/skill callers know resolution behavior.
|
|
319
333
|
|
|
320
|
-
Every mode requires an explicit **`model`** on the request (concrete catalog id
|
|
334
|
+
Every mode requires an explicit **`model`** on the request (concrete catalog id or normalized OpenRouter slug). Unknown models throw `ModelResolutionError`. Profile/choice aliases throw `GatewayAliasModelRejectedError` at ingress — resolve them upstream (ai-tasks).
|
|
321
335
|
|
|
322
336
|
---
|
|
323
337
|
|
|
@@ -332,7 +346,7 @@ Every mode requires an explicit **`model`** on the request (concrete catalog id
|
|
|
332
346
|
| `npm run test:flex-md-esm-regression` | ESM build regression for flex-md |
|
|
333
347
|
| `npm run test:prepublish` | `build` + `npm test` |
|
|
334
348
|
|
|
335
|
-
Live tests use `LIVE_TEST_PROVIDER` / `LIVE_TEST_MODEL` (default `openrouter` + `openai/gpt-4o-mini`). Set `
|
|
349
|
+
Live tests use `LIVE_TEST_PROVIDER` / `LIVE_TEST_MODEL` (default `openrouter` + `openai/gpt-4o-mini`). Set `LIVE_SKIP_INVOKE=1` to skip the LLM call. Profile alias invokes (`LIVE_TEST_PROFILES=1`) are **no longer supported** — resolve profiles upstream (ai-tasks) before calling the gateway.
|
|
336
350
|
|
|
337
351
|
---
|
|
338
352
|
|
package/dist/gateway-utils.js
CHANGED
|
@@ -7,7 +7,8 @@ import { FallbackExhaustedError } from '@x12i/ai-providers-router';
|
|
|
7
7
|
import { ModelResolutionError, ModelProfileInputRejectedError, ModelProfileUnroutableError, resolveInvokeModel, } from '@x12i/ai-tools';
|
|
8
8
|
import { extractHttpStatusCode } from './gateway-retry.js';
|
|
9
9
|
import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
|
|
10
|
-
import { MaxTokensRequiredError, ModelRequiredError } from './instruction-errors.js';
|
|
10
|
+
import { MaxTokensRequiredError, ModelRequiredError, } from './instruction-errors.js';
|
|
11
|
+
import { normalizeInvokeModelAtIngress } from './invoke-model-ingress.js';
|
|
11
12
|
import { DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, GATEWAY_DEFAULT_FREQUENCY_PENALTY, GATEWAY_DEFAULT_PRESENCE_PENALTY, GATEWAY_DEFAULT_TEMPERATURE, GATEWAY_DEFAULT_TOP_P } from './gateway-defaults.js';
|
|
12
13
|
function getPreParsedInstructions(instructions) {
|
|
13
14
|
return instructions ?? '';
|
|
@@ -93,14 +94,30 @@ export async function mergeConfig(request, config, logger, mergeOptions) {
|
|
|
93
94
|
provider: modelConfigAsConfig?.provider || request.config?.provider || internalDefaults?.engine || config.defaultEngine
|
|
94
95
|
};
|
|
95
96
|
const explicitModel = merged.model;
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
let originalProvider = merged.provider;
|
|
98
|
+
let originalModel = explicitModel;
|
|
98
99
|
if (!explicitModel) {
|
|
99
100
|
throw new ModelRequiredError();
|
|
100
101
|
}
|
|
102
|
+
const ingress = normalizeInvokeModelAtIngress(merged.provider, explicitModel, {
|
|
103
|
+
useOpenRouter: mergeOptions?.useOpenRouter,
|
|
104
|
+
});
|
|
105
|
+
if (ingress.changed) {
|
|
106
|
+
logger.verbose('Invoke model normalized at ingress', {
|
|
107
|
+
jobId: request.identity.jobId,
|
|
108
|
+
originalProvider,
|
|
109
|
+
originalModel,
|
|
110
|
+
provider: ingress.provider,
|
|
111
|
+
model: ingress.model,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
merged.provider = ingress.provider;
|
|
115
|
+
merged.model = ingress.model;
|
|
116
|
+
originalProvider = merged.provider;
|
|
117
|
+
originalModel = merged.model;
|
|
101
118
|
if (resolveModels && mergeOptions?.catalog) {
|
|
102
119
|
try {
|
|
103
|
-
const resolved = await resolveInvokeModel({ provider: merged.provider, model:
|
|
120
|
+
const resolved = await resolveInvokeModel({ provider: merged.provider, model: merged.model }, {
|
|
104
121
|
catalog: mergeOptions.catalog,
|
|
105
122
|
routingEnv: mergeOptions.routingEnv,
|
|
106
123
|
openRouterApiKey: mergeOptions.openRouterApiKey,
|
|
@@ -142,7 +159,7 @@ export async function mergeConfig(request, config, logger, mergeOptions) {
|
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
else if (mergeOptions?.openRouterApiKey) {
|
|
145
|
-
const resolved = await resolveInvokeModel({ provider: merged.provider, model:
|
|
162
|
+
const resolved = await resolveInvokeModel({ provider: merged.provider, model: merged.model }, {
|
|
146
163
|
resolveModels: false,
|
|
147
164
|
routingEnv: mergeOptions.routingEnv,
|
|
148
165
|
openRouterApiKey: mergeOptions.openRouterApiKey,
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export type { RequestInterceptor, ResponseInterceptor } from '@x12i/ai-providers
|
|
|
14
14
|
export type { UsageTracker } from '@x12i/ai-providers-router';
|
|
15
15
|
export * from '@x12i/ai-providers-router';
|
|
16
16
|
export { AIGateway } from './gateway.js';
|
|
17
|
-
export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError, MaxTokensRequiredError } from './instruction-errors.js';
|
|
17
|
+
export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError, MaxTokensRequiredError, GatewayAliasModelRejectedError, } from './instruction-errors.js';
|
|
18
|
+
export { normalizeInvokeModelAtIngress } from './invoke-model-ingress.js';
|
|
18
19
|
export { autoRegisterProviders } from './gateway-provider-auto-register.js';
|
|
19
20
|
export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayFallbackAttempt, GatewayTraceRequestIds, GatewayTraceAttempt, GatewayTraceUsageSummary, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions } from './types.js';
|
|
20
21
|
export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, tryExtractFallbackAttemptsFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, buildGatewayPricingRecord, mapAiCostResultToResolvedActivityCost, catalogPricingSucceeded, extractUsageExtrasFromRouterResponse, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, hasNonZeroTokenUsage, MODEL_PROFILE_UNROUTABLE, ModelProfileUnroutableError, ModelProfileInputRejectedError, buildGatewayFallbackAttemptsFromTrace, formatFallbackExhaustionMessage, logResolvedModelRouting, mapGatewayFallbackAttemptsToRouter } from './gateway-utils.js';
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,8 @@ export { ProviderNotFoundError, FallbackExhaustedError } from '@x12i/ai-provider
|
|
|
15
15
|
export * from '@x12i/ai-providers-router';
|
|
16
16
|
// Export enhanced gateway
|
|
17
17
|
export { AIGateway } from './gateway.js';
|
|
18
|
-
export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError, MaxTokensRequiredError } from './instruction-errors.js';
|
|
18
|
+
export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError, MaxTokensRequiredError, GatewayAliasModelRejectedError, } from './instruction-errors.js';
|
|
19
|
+
export { normalizeInvokeModelAtIngress } from './invoke-model-ingress.js';
|
|
19
20
|
export { autoRegisterProviders } from './gateway-provider-auto-register.js';
|
|
20
21
|
export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, tryExtractFallbackAttemptsFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, buildGatewayPricingRecord, mapAiCostResultToResolvedActivityCost, catalogPricingSucceeded, extractUsageExtrasFromRouterResponse, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, hasNonZeroTokenUsage, MODEL_PROFILE_UNROUTABLE, ModelProfileUnroutableError, ModelProfileInputRejectedError, buildGatewayFallbackAttemptsFromTrace, formatFallbackExhaustionMessage, logResolvedModelRouting, mapGatewayFallbackAttemptsToRouter } from './gateway-utils.js';
|
|
21
22
|
export { getGatewayOperationalMode, isProdGatewayMode, parseModelProviderSpec } from './gateway-mode.js';
|
|
@@ -9,6 +9,11 @@ export declare class MaxTokensRequiredError extends Error {
|
|
|
9
9
|
readonly code = "MAX_TOKENS_REQUIRED";
|
|
10
10
|
constructor(message?: string);
|
|
11
11
|
}
|
|
12
|
+
export declare class GatewayAliasModelRejectedError extends Error {
|
|
13
|
+
readonly aliasModel: string;
|
|
14
|
+
readonly code = "GATEWAY_ALIAS_MODEL_REJECTED";
|
|
15
|
+
constructor(aliasModel: string, message?: string);
|
|
16
|
+
}
|
|
12
17
|
export declare class InstructionNotFoundError extends Error {
|
|
13
18
|
key: string;
|
|
14
19
|
backend: string;
|
|
@@ -15,6 +15,16 @@ export class MaxTokensRequiredError extends Error {
|
|
|
15
15
|
this.name = 'MaxTokensRequiredError';
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
export class GatewayAliasModelRejectedError extends Error {
|
|
19
|
+
aliasModel;
|
|
20
|
+
code = 'GATEWAY_ALIAS_MODEL_REJECTED';
|
|
21
|
+
constructor(aliasModel, message) {
|
|
22
|
+
super(message ??
|
|
23
|
+
`Profile/choice alias "${aliasModel}" cannot be used at invoke ingress. Resolve the alias upstream (ai-tasks / resolveAIProfile) before calling the gateway.`);
|
|
24
|
+
this.aliasModel = aliasModel;
|
|
25
|
+
this.name = 'GatewayAliasModelRejectedError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
18
28
|
export class InstructionNotFoundError extends Error {
|
|
19
29
|
key;
|
|
20
30
|
backend;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invoke model ingress normalization (@x12i/ai-profiles `normalizeInvokeModel`).
|
|
3
|
+
* Defense in depth before catalog lookup / `resolveInvokeModel`.
|
|
4
|
+
*/
|
|
5
|
+
export type InvokeModelIngressOptions = {
|
|
6
|
+
useOpenRouter?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type InvokeModelIngressResult = {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
changed: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Normalizes invoke wire shape at gateway ingress. Idempotent when upstream already fixed.
|
|
15
|
+
* Rejects profile/choice aliases — callers must resolve via ai-tasks / `resolveAIProfile`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeInvokeModelAtIngress(provider: string | undefined, model: string, options?: InvokeModelIngressOptions): InvokeModelIngressResult;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invoke model ingress normalization (@x12i/ai-profiles `normalizeInvokeModel`).
|
|
3
|
+
* Defense in depth before catalog lookup / `resolveInvokeModel`.
|
|
4
|
+
*/
|
|
5
|
+
import { AIProfilesError, normalizeInvokeModel } from '@x12i/ai-profiles';
|
|
6
|
+
import { GatewayAliasModelRejectedError } from './instruction-errors.js';
|
|
7
|
+
function looksLikeOpenRouterCompositeSlug(model, provider) {
|
|
8
|
+
const trimmed = model.trim();
|
|
9
|
+
if (trimmed.startsWith('openrouter/'))
|
|
10
|
+
return true;
|
|
11
|
+
const providerHint = provider?.trim().toLowerCase();
|
|
12
|
+
if ((!providerHint || providerHint === 'unspecified') && trimmed.includes('/')) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
function augmentShapeInvalidMessage(err, model, provider) {
|
|
18
|
+
if (err.code !== 'INVOKE_MODEL_SHAPE_INVALID')
|
|
19
|
+
return err;
|
|
20
|
+
if (!looksLikeOpenRouterCompositeSlug(model, provider))
|
|
21
|
+
return err;
|
|
22
|
+
return new AIProfilesError(err.code, `${err.message} Model looks like openrouter/vendor/model composite — split into provider + model.`, err.details);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalizes invoke wire shape at gateway ingress. Idempotent when upstream already fixed.
|
|
26
|
+
* Rejects profile/choice aliases — callers must resolve via ai-tasks / `resolveAIProfile`.
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeInvokeModelAtIngress(provider, model, options = {}) {
|
|
29
|
+
const inputProvider = provider?.trim();
|
|
30
|
+
const inputModel = model.trim();
|
|
31
|
+
try {
|
|
32
|
+
const normalized = normalizeInvokeModel({
|
|
33
|
+
model: inputModel,
|
|
34
|
+
provider: inputProvider,
|
|
35
|
+
routing: 'auto',
|
|
36
|
+
useOpenRouter: options.useOpenRouter,
|
|
37
|
+
}, { useOpenRouter: options.useOpenRouter });
|
|
38
|
+
const changed = normalized.provider !== (inputProvider?.toLowerCase() ?? inputProvider) ||
|
|
39
|
+
normalized.model !== inputModel;
|
|
40
|
+
return {
|
|
41
|
+
provider: normalized.provider,
|
|
42
|
+
model: normalized.model,
|
|
43
|
+
changed,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err instanceof AIProfilesError) {
|
|
48
|
+
if (err.code === 'INVOKE_MODEL_ALIAS_AT_GATEWAY') {
|
|
49
|
+
const alias = typeof err.details?.model === 'string' ? err.details.model : inputModel;
|
|
50
|
+
throw new GatewayAliasModelRejectedError(alias);
|
|
51
|
+
}
|
|
52
|
+
throw augmentShapeInvalidMessage(err, inputModel, inputProvider);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -7,7 +7,8 @@ import { FallbackExhaustedError } from '@x12i/ai-providers-router';
|
|
|
7
7
|
import { ModelResolutionError, ModelProfileInputRejectedError, ModelProfileUnroutableError, resolveInvokeModel, } from '@x12i/ai-tools';
|
|
8
8
|
import { extractHttpStatusCode } from './gateway-retry.js';
|
|
9
9
|
import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
|
|
10
|
-
import { MaxTokensRequiredError, ModelRequiredError } from './instruction-errors.js';
|
|
10
|
+
import { MaxTokensRequiredError, ModelRequiredError, } from './instruction-errors.js';
|
|
11
|
+
import { normalizeInvokeModelAtIngress } from './invoke-model-ingress.js';
|
|
11
12
|
import { DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, GATEWAY_DEFAULT_FREQUENCY_PENALTY, GATEWAY_DEFAULT_PRESENCE_PENALTY, GATEWAY_DEFAULT_TEMPERATURE, GATEWAY_DEFAULT_TOP_P } from './gateway-defaults.js';
|
|
12
13
|
function getPreParsedInstructions(instructions) {
|
|
13
14
|
return instructions ?? '';
|
|
@@ -93,14 +94,30 @@ export async function mergeConfig(request, config, logger, mergeOptions) {
|
|
|
93
94
|
provider: modelConfigAsConfig?.provider || request.config?.provider || internalDefaults?.engine || config.defaultEngine
|
|
94
95
|
};
|
|
95
96
|
const explicitModel = merged.model;
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
let originalProvider = merged.provider;
|
|
98
|
+
let originalModel = explicitModel;
|
|
98
99
|
if (!explicitModel) {
|
|
99
100
|
throw new ModelRequiredError();
|
|
100
101
|
}
|
|
102
|
+
const ingress = normalizeInvokeModelAtIngress(merged.provider, explicitModel, {
|
|
103
|
+
useOpenRouter: mergeOptions?.useOpenRouter,
|
|
104
|
+
});
|
|
105
|
+
if (ingress.changed) {
|
|
106
|
+
logger.verbose('Invoke model normalized at ingress', {
|
|
107
|
+
jobId: request.identity.jobId,
|
|
108
|
+
originalProvider,
|
|
109
|
+
originalModel,
|
|
110
|
+
provider: ingress.provider,
|
|
111
|
+
model: ingress.model,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
merged.provider = ingress.provider;
|
|
115
|
+
merged.model = ingress.model;
|
|
116
|
+
originalProvider = merged.provider;
|
|
117
|
+
originalModel = merged.model;
|
|
101
118
|
if (resolveModels && mergeOptions?.catalog) {
|
|
102
119
|
try {
|
|
103
|
-
const resolved = await resolveInvokeModel({ provider: merged.provider, model:
|
|
120
|
+
const resolved = await resolveInvokeModel({ provider: merged.provider, model: merged.model }, {
|
|
104
121
|
catalog: mergeOptions.catalog,
|
|
105
122
|
routingEnv: mergeOptions.routingEnv,
|
|
106
123
|
openRouterApiKey: mergeOptions.openRouterApiKey,
|
|
@@ -142,7 +159,7 @@ export async function mergeConfig(request, config, logger, mergeOptions) {
|
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
else if (mergeOptions?.openRouterApiKey) {
|
|
145
|
-
const resolved = await resolveInvokeModel({ provider: merged.provider, model:
|
|
162
|
+
const resolved = await resolveInvokeModel({ provider: merged.provider, model: merged.model }, {
|
|
146
163
|
resolveModels: false,
|
|
147
164
|
routingEnv: mergeOptions.routingEnv,
|
|
148
165
|
openRouterApiKey: mergeOptions.openRouterApiKey,
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -15,7 +15,8 @@ export { ProviderNotFoundError, FallbackExhaustedError } from '@x12i/ai-provider
|
|
|
15
15
|
export * from '@x12i/ai-providers-router';
|
|
16
16
|
// Export enhanced gateway
|
|
17
17
|
export { AIGateway } from './gateway.js';
|
|
18
|
-
export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError, MaxTokensRequiredError } from './instruction-errors.js';
|
|
18
|
+
export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError, MaxTokensRequiredError, GatewayAliasModelRejectedError, } from './instruction-errors.js';
|
|
19
|
+
export { normalizeInvokeModelAtIngress } from './invoke-model-ingress.js';
|
|
19
20
|
export { autoRegisterProviders } from './gateway-provider-auto-register.js';
|
|
20
21
|
export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, tryExtractFallbackAttemptsFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, buildGatewayPricingRecord, mapAiCostResultToResolvedActivityCost, catalogPricingSucceeded, extractUsageExtrasFromRouterResponse, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, hasNonZeroTokenUsage, MODEL_PROFILE_UNROUTABLE, ModelProfileUnroutableError, ModelProfileInputRejectedError, buildGatewayFallbackAttemptsFromTrace, formatFallbackExhaustionMessage, logResolvedModelRouting, mapGatewayFallbackAttemptsToRouter } from './gateway-utils.js';
|
|
21
22
|
export { getGatewayOperationalMode, isProdGatewayMode, parseModelProviderSpec } from './gateway-mode.js';
|
package/dist-cjs/index.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export type { RequestInterceptor, ResponseInterceptor } from '@x12i/ai-providers
|
|
|
14
14
|
export type { UsageTracker } from '@x12i/ai-providers-router';
|
|
15
15
|
export * from '@x12i/ai-providers-router';
|
|
16
16
|
export { AIGateway } from './gateway.js';
|
|
17
|
-
export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError, MaxTokensRequiredError } from './instruction-errors.js';
|
|
17
|
+
export { InstructionNotFoundError, InstructionBackendError, ModelRequiredError, MaxTokensRequiredError, GatewayAliasModelRejectedError, } from './instruction-errors.js';
|
|
18
|
+
export { normalizeInvokeModelAtIngress } from './invoke-model-ingress.js';
|
|
18
19
|
export { autoRegisterProviders } from './gateway-provider-auto-register.js';
|
|
19
20
|
export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayFallbackAttempt, GatewayTraceRequestIds, GatewayTraceAttempt, GatewayTraceUsageSummary, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions } from './types.js';
|
|
20
21
|
export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, tryExtractFallbackAttemptsFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, resolveCostCompletionWithAiTools, buildGatewayPricingRecord, mapAiCostResultToResolvedActivityCost, catalogPricingSucceeded, extractUsageExtrasFromRouterResponse, buildTraceUsageSummary, enrichTraceAttemptsWithBilling, hasNonZeroTokenUsage, MODEL_PROFILE_UNROUTABLE, ModelProfileUnroutableError, ModelProfileInputRejectedError, buildGatewayFallbackAttemptsFromTrace, formatFallbackExhaustionMessage, logResolvedModelRouting, mapGatewayFallbackAttemptsToRouter } from './gateway-utils.js';
|
|
@@ -15,6 +15,16 @@ export class MaxTokensRequiredError extends Error {
|
|
|
15
15
|
this.name = 'MaxTokensRequiredError';
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
export class GatewayAliasModelRejectedError extends Error {
|
|
19
|
+
aliasModel;
|
|
20
|
+
code = 'GATEWAY_ALIAS_MODEL_REJECTED';
|
|
21
|
+
constructor(aliasModel, message) {
|
|
22
|
+
super(message ??
|
|
23
|
+
`Profile/choice alias "${aliasModel}" cannot be used at invoke ingress. Resolve the alias upstream (ai-tasks / resolveAIProfile) before calling the gateway.`);
|
|
24
|
+
this.aliasModel = aliasModel;
|
|
25
|
+
this.name = 'GatewayAliasModelRejectedError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
18
28
|
export class InstructionNotFoundError extends Error {
|
|
19
29
|
key;
|
|
20
30
|
backend;
|
|
@@ -9,6 +9,11 @@ export declare class MaxTokensRequiredError extends Error {
|
|
|
9
9
|
readonly code = "MAX_TOKENS_REQUIRED";
|
|
10
10
|
constructor(message?: string);
|
|
11
11
|
}
|
|
12
|
+
export declare class GatewayAliasModelRejectedError extends Error {
|
|
13
|
+
readonly aliasModel: string;
|
|
14
|
+
readonly code = "GATEWAY_ALIAS_MODEL_REJECTED";
|
|
15
|
+
constructor(aliasModel: string, message?: string);
|
|
16
|
+
}
|
|
12
17
|
export declare class InstructionNotFoundError extends Error {
|
|
13
18
|
key: string;
|
|
14
19
|
backend: string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invoke model ingress normalization (@x12i/ai-profiles `normalizeInvokeModel`).
|
|
3
|
+
* Defense in depth before catalog lookup / `resolveInvokeModel`.
|
|
4
|
+
*/
|
|
5
|
+
import { AIProfilesError, normalizeInvokeModel } from '@x12i/ai-profiles';
|
|
6
|
+
import { GatewayAliasModelRejectedError } from './instruction-errors.js';
|
|
7
|
+
function looksLikeOpenRouterCompositeSlug(model, provider) {
|
|
8
|
+
const trimmed = model.trim();
|
|
9
|
+
if (trimmed.startsWith('openrouter/'))
|
|
10
|
+
return true;
|
|
11
|
+
const providerHint = provider?.trim().toLowerCase();
|
|
12
|
+
if ((!providerHint || providerHint === 'unspecified') && trimmed.includes('/')) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
function augmentShapeInvalidMessage(err, model, provider) {
|
|
18
|
+
if (err.code !== 'INVOKE_MODEL_SHAPE_INVALID')
|
|
19
|
+
return err;
|
|
20
|
+
if (!looksLikeOpenRouterCompositeSlug(model, provider))
|
|
21
|
+
return err;
|
|
22
|
+
return new AIProfilesError(err.code, `${err.message} Model looks like openrouter/vendor/model composite — split into provider + model.`, err.details);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalizes invoke wire shape at gateway ingress. Idempotent when upstream already fixed.
|
|
26
|
+
* Rejects profile/choice aliases — callers must resolve via ai-tasks / `resolveAIProfile`.
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeInvokeModelAtIngress(provider, model, options = {}) {
|
|
29
|
+
const inputProvider = provider?.trim();
|
|
30
|
+
const inputModel = model.trim();
|
|
31
|
+
try {
|
|
32
|
+
const normalized = normalizeInvokeModel({
|
|
33
|
+
model: inputModel,
|
|
34
|
+
provider: inputProvider,
|
|
35
|
+
routing: 'auto',
|
|
36
|
+
useOpenRouter: options.useOpenRouter,
|
|
37
|
+
}, { useOpenRouter: options.useOpenRouter });
|
|
38
|
+
const changed = normalized.provider !== (inputProvider?.toLowerCase() ?? inputProvider) ||
|
|
39
|
+
normalized.model !== inputModel;
|
|
40
|
+
return {
|
|
41
|
+
provider: normalized.provider,
|
|
42
|
+
model: normalized.model,
|
|
43
|
+
changed,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err instanceof AIProfilesError) {
|
|
48
|
+
if (err.code === 'INVOKE_MODEL_ALIAS_AT_GATEWAY') {
|
|
49
|
+
const alias = typeof err.details?.model === 'string' ? err.details.model : inputModel;
|
|
50
|
+
throw new GatewayAliasModelRejectedError(alias);
|
|
51
|
+
}
|
|
52
|
+
throw augmentShapeInvalidMessage(err, inputModel, inputProvider);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invoke model ingress normalization (@x12i/ai-profiles `normalizeInvokeModel`).
|
|
3
|
+
* Defense in depth before catalog lookup / `resolveInvokeModel`.
|
|
4
|
+
*/
|
|
5
|
+
export type InvokeModelIngressOptions = {
|
|
6
|
+
useOpenRouter?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type InvokeModelIngressResult = {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
changed: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Normalizes invoke wire shape at gateway ingress. Idempotent when upstream already fixed.
|
|
15
|
+
* Rejects profile/choice aliases — callers must resolve via ai-tasks / `resolveAIProfile`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeInvokeModelAtIngress(provider: string | undefined, model: string, options?: InvokeModelIngressOptions): InvokeModelIngressResult;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x12i/ai-gateway",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "AI Gateway - Unified interface for LLM provider routing and management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -41,9 +41,10 @@
|
|
|
41
41
|
"author": "x12i",
|
|
42
42
|
"license": "mit",
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@x12i/activix": "^8.6.
|
|
44
|
+
"@x12i/activix": "^8.6.1",
|
|
45
|
+
"@x12i/ai-profiles": "^3.2.0",
|
|
45
46
|
"@x12i/ai-providers-router": "^4.9.2",
|
|
46
|
-
"@x12i/ai-tools": "^3.
|
|
47
|
+
"@x12i/ai-tools": "^3.1.0",
|
|
47
48
|
"@x12i/flex-md": "^4.8.0",
|
|
48
49
|
"@x12i/logxer": "^4.6.0",
|
|
49
50
|
"@x12i/rendrix": "^4.3.0"
|