@x12i/ai-providers-router 4.8.6 → 4.8.8
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 +73 -22
- package/dist/factory.js +14 -29
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/logger.d.ts +39 -43
- package/dist/logger.js +97 -88
- package/dist/router/Router.d.ts +3 -1
- package/dist/router/Router.js +37 -40
- package/dist/router/RouterTypes.d.ts +15 -2
- package/dist/router/RouterWrapper.d.ts +1 -0
- package/dist/router/RouterWrapper.js +25 -8
- package/dist/utils/openrouterEnv.d.ts +18 -0
- package/dist/utils/openrouterEnv.js +28 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ AIRouterRequest
|
|
|
137
137
|
| ID | Role |
|
|
138
138
|
|----|------|
|
|
139
139
|
| `openrouter` | Explicit OpenRouter transport |
|
|
140
|
-
| Any vendor ID | Routed through OpenRouter when
|
|
140
|
+
| Any vendor ID | Routed through OpenRouter when preferred (`USE_OPENROUTER=true`, default) or as fallback when no direct key |
|
|
141
141
|
|
|
142
142
|
> **Grok ≠ Groq** — Grok is xAI (`grok` / `xai`). Groq is GroqCloud (`groq`).
|
|
143
143
|
|
|
@@ -145,7 +145,7 @@ AIRouterRequest
|
|
|
145
145
|
|
|
146
146
|
## OpenRouter mode
|
|
147
147
|
|
|
148
|
-
OpenRouter is a unified API gateway.
|
|
148
|
+
OpenRouter is a unified API gateway. With an `OPENROUTER_API_KEY`, the router can reach models from many vendors through one key — using familiar provider names (`openai`, `grok`, `anthropic`, …) and automatic model mapping (e.g. `openai` + `gpt-4o` → `openai/gpt-4o`).
|
|
149
149
|
|
|
150
150
|
### Enable OpenRouter
|
|
151
151
|
|
|
@@ -157,26 +157,52 @@ export OPENROUTER_API_KEY=sk-or-your-key-here
|
|
|
157
157
|
# export OPEN_ROUTER_KEY=sk-or-your-key-here
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
Optional ranking headers:
|
|
161
161
|
|
|
162
162
|
```bash
|
|
163
|
-
export
|
|
163
|
+
export OPENROUTER_HTTP_REFERER=https://your-site.com # legacy: OPEN_ROUTER_HTTP_REFERER
|
|
164
|
+
export OPENROUTER_X_TITLE=Your Site Name # legacy: OPEN_ROUTER_X_TITLE
|
|
164
165
|
```
|
|
165
166
|
|
|
166
|
-
|
|
167
|
+
### `USE_OPENROUTER` — prefer vs fallback
|
|
168
|
+
|
|
169
|
+
`USE_OPENROUTER` does **not** turn OpenRouter on or off. The router always registers OpenRouter when a key is present. This flag controls **whether OpenRouter is preferred over direct provider keys**.
|
|
170
|
+
|
|
171
|
+
| `USE_OPENROUTER` | `OPENROUTER_API_KEY` | Direct provider key (e.g. `OPENAI_API_KEY`) | What happens |
|
|
172
|
+
|------------------|----------------------|---------------------------------------------|--------------|
|
|
173
|
+
| unset or `true` *(default)* | set | set or unset | **Prefer OpenRouter** — all vendor calls route through OpenRouter, even when a direct key exists |
|
|
174
|
+
| unset or `true` | set | not set | Route through OpenRouter |
|
|
175
|
+
| `false` | set | set for requested vendor | **Direct provider** — use the vendor's own key/API |
|
|
176
|
+
| `false` | set | not set for requested vendor | **OpenRouter fallback** — e.g. request `anthropic` with no `ANTHROPIC_API_KEY` still works via OpenRouter |
|
|
177
|
+
|
|
178
|
+
**Default:** prefer OpenRouter whenever `OPENROUTER_API_KEY` is set (`USE_OPENROUTER` defaults to `true`).
|
|
179
|
+
|
|
180
|
+
To use direct provider keys when available, while keeping OpenRouter as fallback for vendors without keys:
|
|
167
181
|
|
|
168
182
|
```bash
|
|
169
|
-
export
|
|
170
|
-
export
|
|
183
|
+
export USE_OPENROUTER=false
|
|
184
|
+
export OPENROUTER_API_KEY=sk-or-...
|
|
185
|
+
export OPENAI_API_KEY=sk-... # openai requests → direct OpenAI
|
|
186
|
+
# no ANTHROPIC_API_KEY # anthropic requests → OpenRouter fallback
|
|
171
187
|
```
|
|
172
188
|
|
|
173
|
-
|
|
189
|
+
Programmatic override:
|
|
174
190
|
|
|
191
|
+
```ts
|
|
192
|
+
const router = await createRouter({
|
|
193
|
+
useOpenRouter: false, // direct when keys exist; OpenRouter fallback otherwise
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Behavior summary
|
|
198
|
+
|
|
199
|
+
- **OpenRouter is always available** when `OPENROUTER_API_KEY` is set — used as the default transport or as fallback
|
|
200
|
+
- **`USE_OPENROUTER=true` (default):** routes through OpenRouter even if `OPENAI_API_KEY`, `GROK_API_KEY`, etc. are also set; direct provider packages are not auto-registered (avoids singleton config conflicts)
|
|
201
|
+
- **`USE_OPENROUTER=false`:** auto-registers direct providers when their API keys exist; OpenRouter handles any vendor without a direct key
|
|
175
202
|
- Works with `createRouter()` and `new LLMProviderRouter()` — auto-registration on first call
|
|
176
|
-
- Provider names stay the same in your code; the router handles
|
|
203
|
+
- Provider names stay the same in your code; the router handles transport selection internally
|
|
177
204
|
- Catalog data (`.metadata/openrouter_catalog_with_vendor_mapping.json`) drives model validation and provider inference
|
|
178
|
-
-
|
|
179
|
-
- Responses are parsed directly from OpenAI-compatible formats (no `ai-io-normalizer` on the OpenRouter path)
|
|
205
|
+
- Responses on the OpenRouter path are parsed directly from OpenAI-compatible formats (no `ai-io-normalizer`)
|
|
180
206
|
|
|
181
207
|
### Examples
|
|
182
208
|
|
|
@@ -245,6 +271,7 @@ const router = await createRouter({
|
|
|
245
271
|
logLevel: 'info',
|
|
246
272
|
verbose: false,
|
|
247
273
|
timeoutMs: 60_000,
|
|
274
|
+
useOpenRouter: true, // default: prefer OpenRouter when OPENROUTER_API_KEY is set
|
|
248
275
|
fallbackChain: [{ provider: 'openai', model: 'gpt-4o-mini' }, { provider: 'grok', model: 'grok-2' }],
|
|
249
276
|
openrouter: { apiKey: 'sk-or-...', httpReferer: 'https://example.com', xTitle: 'My App' },
|
|
250
277
|
usageTracker: {
|
|
@@ -278,9 +305,9 @@ Passing any explicit config object to `createRouter(config)` overrides zero-conf
|
|
|
278
305
|
| `GROK_API_KEY` | — | Required for direct Grok calls |
|
|
279
306
|
| `XAI_API_BASE` | — | Custom xAI base URL |
|
|
280
307
|
| **OpenRouter** | | |
|
|
281
|
-
| `OPENROUTER_API_KEY` | — |
|
|
282
|
-
| `OPEN_ROUTER_KEY` | — | Legacy alias |
|
|
283
|
-
| `USE_OPENROUTER` |
|
|
308
|
+
| `OPENROUTER_API_KEY` | — | Enables OpenRouter (always registered when set) |
|
|
309
|
+
| `OPEN_ROUTER_KEY` | — | Legacy alias for `OPENROUTER_API_KEY` |
|
|
310
|
+
| `USE_OPENROUTER` | `true` | Prefer OpenRouter over direct keys when OR key is set; set `false` to use direct providers when keys exist (OpenRouter remains fallback) |
|
|
284
311
|
| `OPENROUTER_HTTP_REFERER` | — | Optional ranking header |
|
|
285
312
|
| `OPENROUTER_X_TITLE` | — | Optional ranking header |
|
|
286
313
|
| **Other providers** | | |
|
|
@@ -309,20 +336,44 @@ AI_PROVIDER_ROUTER_VERBOSE=true
|
|
|
309
336
|
|
|
310
337
|
**Log levels:** `error` · `warn` · `info` · `debug` · `verbose` · `off`
|
|
311
338
|
|
|
312
|
-
When neither `_LOGS_LEVEL` nor `_LOG_LEVEL` is set
|
|
339
|
+
When neither `_LOGS_LEVEL` nor `_LOG_LEVEL` is set, no `logLevel` / `logging` is passed, and the logxer registry has no entry, the router defaults to **`info`** (not logxer's package-only default of `warn`). `createRouter()` loads `LOGXER_PACKAGE_LEVELS` / `LOGXER_PACKAGE_LOGS_DEFAULT` via `applyPackageLogLevelsFromEnv()` after `.env`.
|
|
313
340
|
|
|
314
|
-
**
|
|
341
|
+
**Host apps (logxer ≥ 4.5)** — provider packages (`@x12i/ai-provider-openai`, etc.) are **not** on the logxer 4.5 stack. Stack/registry options apply **only** to this router's logs (`AI_PROVIDER_ROUTER`). Configure your other libraries from `@x12i/logxer` in the host; pass the same `StackLoggingOptions` into `createRouter` when you want one object for the whole app:
|
|
315
342
|
|
|
316
343
|
```ts
|
|
317
|
-
import {
|
|
344
|
+
import { configurePackageLogLevels, type StackLoggingOptions } from '@x12i/logxer';
|
|
345
|
+
import { createRouter, ROUTER_LOG_ENV_PREFIX } from '@x12i/ai-providers-router';
|
|
346
|
+
|
|
347
|
+
configurePackageLogLevels({
|
|
348
|
+
default: 'warn',
|
|
349
|
+
levels: {
|
|
350
|
+
MY_GATEWAY: 'info',
|
|
351
|
+
[ROUTER_LOG_ENV_PREFIX]: 'debug',
|
|
352
|
+
},
|
|
353
|
+
});
|
|
318
354
|
|
|
319
|
-
const
|
|
355
|
+
const logging: StackLoggingOptions = {
|
|
356
|
+
packageLevels: { [ROUTER_LOG_ENV_PREFIX]: 'debug' },
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const router = await createRouter({ logging, verbose: true });
|
|
360
|
+
```
|
|
320
361
|
|
|
321
|
-
|
|
362
|
+
Bulk env for this package (loaded by `createRouter()` after `.env`):
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
LOGXER_PACKAGE_LEVELS=AI_PROVIDER_ROUTER:info
|
|
366
|
+
AI_PROVIDER_ROUTER_LOGS_LEVEL=error # wins over bulk for this prefix only
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Programmatic (router only):**
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
const router = await createRouter({ logLevel: 'debug', verbose: true });
|
|
322
373
|
const router2 = await createRouter({ logger: createLogger({ level: 'info', verbose: false }) });
|
|
323
374
|
```
|
|
324
375
|
|
|
325
|
-
Verbose mode logs sanitized AI request/response payloads
|
|
376
|
+
Verbose mode logs sanitized AI request/response payloads. Cross-cutting sinks (console, file, format) are configured in the **host** via `@x12i/logxer` — not via provider packages.
|
|
326
377
|
|
|
327
378
|
---
|
|
328
379
|
|
|
@@ -495,7 +546,7 @@ router.addResponseInterceptor(async (res, provider) => {
|
|
|
495
546
|
});
|
|
496
547
|
```
|
|
497
548
|
|
|
498
|
-
OpenRouter
|
|
549
|
+
OpenRouter registers a request interceptor when `USE_OPENROUTER=true` (default) to route vendor calls through OpenRouter while preserving the original provider name for model mapping. When `USE_OPENROUTER=false`, the interceptor is skipped; `resolveProviderName` uses direct providers when registered and falls back to OpenRouter otherwise.
|
|
499
550
|
|
|
500
551
|
### Health checks
|
|
501
552
|
|
|
@@ -586,7 +637,7 @@ const registry = router.getProviderRegistry();
|
|
|
586
637
|
const adapters = router.getAdapterRegistry();
|
|
587
638
|
```
|
|
588
639
|
|
|
589
|
-
Providers are
|
|
640
|
+
Providers are **auto-registered on first invoke** when matching API keys are in the environment. When `USE_OPENROUTER=true` (default) and `OPENROUTER_API_KEY` is set, direct providers are skipped in favor of OpenRouter. With `USE_OPENROUTER=false`, both direct providers and OpenRouter can be registered simultaneously.
|
|
590
641
|
|
|
591
642
|
Legacy config file support:
|
|
592
643
|
|
package/dist/factory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { applyPackageLogLevelsFromEnv } from '@x12i/logxer';
|
|
1
2
|
import { LLMProviderRouter } from './router/RouterWrapper.js';
|
|
2
3
|
import dns from 'node:dns';
|
|
3
|
-
import { resolveOpenRouterApiKey, OPENROUTER_API_KEY_ERC } from './utils/openrouterEnv.js';
|
|
4
|
+
import { resolveOpenRouterApiKey, OPENROUTER_API_KEY_ERC, resolveUseOpenRouter } from './utils/openrouterEnv.js';
|
|
4
5
|
import dotenv from 'dotenv';
|
|
5
6
|
// Fix IPv6-first DNS resolution issue on Windows (forces IPv4-first to avoid connect timeouts)
|
|
6
7
|
// This prevents undici from trying IPv6 first on networks that silently drop IPv6 traffic
|
|
@@ -28,6 +29,7 @@ dns.setDefaultResultOrder('ipv4first');
|
|
|
28
29
|
// This ensures nx-config2 can resolve ENV. placeholders
|
|
29
30
|
// dotenv.config() is idempotent and won't overwrite existing process.env values
|
|
30
31
|
dotenv.config();
|
|
32
|
+
applyPackageLogLevelsFromEnv();
|
|
31
33
|
/**
|
|
32
34
|
* Resolve ENV. placeholders to actual environment variable values
|
|
33
35
|
* Replaces nx-config2 functionality to avoid ESM/CommonJS compatibility issues
|
|
@@ -89,6 +91,7 @@ export async function createRouter(config) {
|
|
|
89
91
|
const ercConfig = {
|
|
90
92
|
// Router-specific configuration
|
|
91
93
|
router: {
|
|
94
|
+
logsLevel: 'ENV.AI_PROVIDER_ROUTER_LOGS_LEVEL',
|
|
92
95
|
logLevel: 'ENV.AI_PROVIDER_ROUTER_LOG_LEVEL||info',
|
|
93
96
|
verbose: 'ENV.AI_PROVIDER_ROUTER_VERBOSE||false',
|
|
94
97
|
timeoutMs: 'ENV.AI_PROVIDER_ROUTER_TIMEOUT_MS||60000',
|
|
@@ -131,41 +134,23 @@ export async function createRouter(config) {
|
|
|
131
134
|
ercResult.config.providers.openrouter.apiKey = resolvedOpenRouterKey;
|
|
132
135
|
}
|
|
133
136
|
// Use explicit config if provided (Advanced Mode), otherwise use ERC auto-discovered config (Zero-Config Mode)
|
|
134
|
-
const
|
|
135
|
-
|
|
137
|
+
const useOpenRouter = resolveUseOpenRouter({
|
|
138
|
+
userValue: config?.useOpenRouter,
|
|
139
|
+
});
|
|
140
|
+
const finalConfig = Object.keys(config || {}).length > 0 ? {
|
|
141
|
+
...config,
|
|
142
|
+
useOpenRouter: config?.useOpenRouter ?? useOpenRouter,
|
|
143
|
+
} : {
|
|
144
|
+
logLevel: (ercResult.config.router.logsLevel ||
|
|
145
|
+
ercResult.config.router.logLevel),
|
|
136
146
|
verbose: ercResult.config.router.verbose === 'true' || ercResult.config.router.verbose === true,
|
|
137
147
|
timeoutMs: ercResult.config.router.timeoutMs ? parseInt(String(ercResult.config.router.timeoutMs), 10) : undefined,
|
|
138
148
|
defaultTimeoutMs: ercResult.config.router.timeoutMs ? parseInt(String(ercResult.config.router.timeoutMs), 10) : undefined,
|
|
149
|
+
useOpenRouter,
|
|
139
150
|
};
|
|
140
151
|
const router = new LLMProviderRouter(finalConfig);
|
|
141
152
|
// Configure providers from ERC auto-detected config OR explicit config
|
|
142
153
|
const providerConfigs = config?.providerConfigs || ercResult.config.providers;
|
|
143
|
-
// Check if OpenRouter mode should be enabled
|
|
144
|
-
// Default: true if OPENROUTER_API_KEY (or legacy OPEN_ROUTER_KEY) is present, else false
|
|
145
|
-
// Can be explicitly disabled with USE_OPENROUTER=false
|
|
146
|
-
const openRouterKey = providerConfigs.openrouter?.apiKey ?? resolvedOpenRouterKey;
|
|
147
|
-
const useOpenRouterEnv = providerConfigs.openrouter?.useOpenRouter;
|
|
148
|
-
// Check if API key is valid (not placeholder, not empty)
|
|
149
|
-
const hasValidKey = openRouterKey &&
|
|
150
|
-
openRouterKey !== 'ENV.OPEN_ROUTER_KEY' &&
|
|
151
|
-
openRouterKey !== OPENROUTER_API_KEY_ERC &&
|
|
152
|
-
typeof openRouterKey === 'string' &&
|
|
153
|
-
openRouterKey.trim() !== '';
|
|
154
|
-
// Determine if OpenRouter should be enabled
|
|
155
|
-
// Default to true if key is present, unless explicitly disabled
|
|
156
|
-
let useOpenRouter = false;
|
|
157
|
-
if (hasValidKey) {
|
|
158
|
-
if (useOpenRouterEnv === undefined || useOpenRouterEnv === '' || useOpenRouterEnv === 'true' || useOpenRouterEnv === true) {
|
|
159
|
-
useOpenRouter = true;
|
|
160
|
-
}
|
|
161
|
-
else if (useOpenRouterEnv === 'false' || useOpenRouterEnv === false) {
|
|
162
|
-
useOpenRouter = false;
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
// Default to true if key is present
|
|
166
|
-
useOpenRouter = true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
154
|
// NOTE: We don't need to manually register providers here anymore if they are
|
|
170
155
|
// in environment variables, as LLMProviderRouter will auto-register them
|
|
171
156
|
// on first invoke/stream/createBatch call.
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export type { FallbackAttempt } from './errors.js';
|
|
|
7
7
|
export type { PartialRouterPayload } from './router/partialErrorPayload.js';
|
|
8
8
|
export type { RequestInterceptor, ResponseInterceptor } from './interceptors.js';
|
|
9
9
|
export type { UsageTracker, AdapterLoader, ProviderInit } from './types.js';
|
|
10
|
-
export { Logger, getLogger, createLogger } from './logger.js';
|
|
11
|
-
export type { LogLevel, LoggerConfig } from './logger.js';
|
|
10
|
+
export { Logger, getLogger, createLogger, createRouterLogxer, ROUTER_LOG_ENV_PREFIX, DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, } from './logger.js';
|
|
11
|
+
export type { LogLevel, LoggerConfig, LogMeta, LogRuntimeContext, StackLoggingOptions } from './logger.js';
|
|
12
12
|
export { AIGateway } from './gateway.js';
|
|
13
13
|
export type { EnhancedLLMResponse } from './gateway.js';
|
|
14
14
|
export { applyResponseNormalization } from './normalization/applyResponseNormalization.js';
|
package/dist/index.js
CHANGED
|
@@ -38,8 +38,8 @@ export {
|
|
|
38
38
|
export { createRouter, createRouterFromConfig } from './factory.js';
|
|
39
39
|
// Error classes
|
|
40
40
|
export { ProviderNotFoundError, FallbackExhaustedError, ProviderNotInstalledError, ProviderTimeoutError } from './errors.js';
|
|
41
|
-
// Logger
|
|
42
|
-
export { Logger, getLogger, createLogger } from './logger.js';
|
|
41
|
+
// Logger (logxer correlation; host `StackLoggingOptions` via createRouter / LoggerConfig)
|
|
42
|
+
export { Logger, getLogger, createLogger, createRouterLogxer, ROUTER_LOG_ENV_PREFIX, DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, } from './logger.js';
|
|
43
43
|
// Gateway (thin invoke wrapper)
|
|
44
44
|
export { AIGateway } from './gateway.js';
|
|
45
45
|
// Response normalization (Run Analysis G6/G8)
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logging utility for AI Provider Router
|
|
3
|
-
*
|
|
3
|
+
* Wraps @x12i/logxer with package config, verbose AI payload logging, and correlation context.
|
|
4
4
|
*/
|
|
5
|
-
import { type LogLevel } from '@x12i/logxer';
|
|
6
|
-
export type { LogLevel };
|
|
5
|
+
import { applyPackageLogLevelsFromEnv, DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, type Logxer, type LogLevel, type LogMeta, type LogRuntimeContext, type StackLoggingOptions, type ScopeCriteria, type ScopeLogsOptions, type ScopeLogsResult, type GetJobLogsInput, type GetJobLogsResult } from '@x12i/logxer';
|
|
6
|
+
export type { LogLevel, LogMeta, LogRuntimeContext, StackLoggingOptions };
|
|
7
|
+
export { DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, };
|
|
8
|
+
/** Stable logxer `envPrefix` for this package (`AI_PROVIDER_ROUTER_LOGS_LEVEL`). */
|
|
9
|
+
export declare const ROUTER_LOG_ENV_PREFIX = "AI_PROVIDER_ROUTER";
|
|
7
10
|
export interface LoggerConfig {
|
|
8
11
|
verbose?: boolean;
|
|
12
|
+
/** Explicit instance level (wins over `logging` stack and registry). */
|
|
9
13
|
level?: LogLevel;
|
|
14
|
+
/**
|
|
15
|
+
* From the **host application** (logxer ≥ 4.5). Resolved only for `AI_PROVIDER_ROUTER`.
|
|
16
|
+
* Does not configure provider dependencies (they are not on the logxer 4.5 stack).
|
|
17
|
+
*/
|
|
18
|
+
logging?: StackLoggingOptions;
|
|
10
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a logxer instance for this package (prefer over ad-hoc `createLogxer` in consumers).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createRouterLogxer(options?: {
|
|
24
|
+
logging?: StackLoggingOptions;
|
|
25
|
+
level?: LogLevel;
|
|
26
|
+
}): Logxer;
|
|
11
27
|
/**
|
|
12
28
|
* Logger class that wraps logxer with proper log levels
|
|
13
29
|
*/
|
|
@@ -15,53 +31,33 @@ export declare class Logger {
|
|
|
15
31
|
verbose: boolean;
|
|
16
32
|
level: LogLevel;
|
|
17
33
|
private gateway;
|
|
34
|
+
private logging?;
|
|
18
35
|
constructor(config?: LoggerConfig);
|
|
19
36
|
/**
|
|
20
37
|
* Update logger configuration
|
|
21
38
|
*/
|
|
22
39
|
setConfig(config: LoggerConfig): void;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
info(message: string, data?:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
logVerbose(message: string, data?: Record<string, unknown>): void;
|
|
43
|
-
/**
|
|
44
|
-
* Log AI request (unfiltered, only in verbose mode)
|
|
45
|
-
*/
|
|
46
|
-
logAIRequest(provider: string, request: unknown, metadata?: Record<string, unknown>): void;
|
|
47
|
-
/**
|
|
48
|
-
* Log AI response (unfiltered, only in verbose mode)
|
|
49
|
-
*/
|
|
50
|
-
logAIResponse(provider: string, response: unknown, metadata?: Record<string, unknown>): void;
|
|
51
|
-
/**
|
|
52
|
-
* Log AI request/response pair (unfiltered, only in verbose mode)
|
|
53
|
-
*/
|
|
54
|
-
logAIIteraction(provider: string, request: unknown, response: unknown, duration?: number, metadata?: Record<string, unknown>): void;
|
|
55
|
-
/**
|
|
56
|
-
* Sanitize data for logging (remove sensitive info, handle circular refs)
|
|
40
|
+
getConfig(): Readonly<ReturnType<Logxer['getConfig']>>;
|
|
41
|
+
isLevelEnabled(level: LogLevel): boolean;
|
|
42
|
+
infoCode(code: string, data?: LogMeta): void;
|
|
43
|
+
warnCode(code: string, data?: LogMeta): void;
|
|
44
|
+
errorCode(code: string, data?: LogMeta): void;
|
|
45
|
+
diagnostic(level: LogLevel, message: string, data?: LogMeta): void;
|
|
46
|
+
success(message: string, data?: LogMeta): void;
|
|
47
|
+
scopeLogs(criteria: ScopeCriteria, options?: ScopeLogsOptions): Promise<ScopeLogsResult>;
|
|
48
|
+
getJobLogs(input: GetJobLogsInput): Promise<GetJobLogsResult>;
|
|
49
|
+
error(message: string, data?: LogMeta): void;
|
|
50
|
+
warn(message: string, data?: LogMeta): void;
|
|
51
|
+
info(message: string, data?: LogMeta): void;
|
|
52
|
+
debug(message: string, data?: LogMeta): void;
|
|
53
|
+
logVerbose(message: string, data?: LogMeta): void;
|
|
54
|
+
logAIRequest(provider: string, request: unknown, metadata?: LogMeta): void;
|
|
55
|
+
logAIResponse(provider: string, response: unknown, metadata?: LogMeta): void;
|
|
56
|
+
logAIIteraction(provider: string, request: unknown, response: unknown, duration?: number, metadata?: LogMeta): void;
|
|
57
|
+
/**
|
|
58
|
+
* @deprecated Logxer sanitizes payloads on emit when `sanitization.enabled` is set (default for this logger).
|
|
57
59
|
*/
|
|
58
60
|
sanitizeForLogging(data: unknown): unknown;
|
|
59
61
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Get or create the default logger instance
|
|
62
|
-
*/
|
|
63
62
|
export declare function getLogger(config?: LoggerConfig): Logger;
|
|
64
|
-
/**
|
|
65
|
-
* Create a new logger instance
|
|
66
|
-
*/
|
|
67
63
|
export declare function createLogger(config?: LoggerConfig): Logger;
|
package/dist/logger.js
CHANGED
|
@@ -1,15 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logging utility for AI Provider Router
|
|
3
|
-
*
|
|
3
|
+
* Wraps @x12i/logxer with package config, verbose AI payload logging, and correlation context.
|
|
4
4
|
*/
|
|
5
|
-
import { createLogxer } from '@x12i/logxer';
|
|
5
|
+
import { applyPackageLogLevelsFromEnv, createLogxer, DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, } from '@x12i/logxer';
|
|
6
|
+
export { DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, };
|
|
7
|
+
/** Stable logxer `envPrefix` for this package (`AI_PROVIDER_ROUTER_LOGS_LEVEL`). */
|
|
8
|
+
export const ROUTER_LOG_ENV_PREFIX = 'AI_PROVIDER_ROUTER';
|
|
6
9
|
const LOGXER_PACKAGE = {
|
|
7
10
|
packageName: 'AI Provider Router',
|
|
8
|
-
envPrefix:
|
|
11
|
+
envPrefix: ROUTER_LOG_ENV_PREFIX,
|
|
9
12
|
debugNamespace: 'ai-providers-router',
|
|
10
13
|
};
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
const DEFAULT_LOGXER_OPTIONS = {
|
|
15
|
+
sanitization: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
detectAPIKeys: true,
|
|
18
|
+
detectPasswords: true,
|
|
19
|
+
detectJWTs: true,
|
|
20
|
+
detectEmails: true,
|
|
21
|
+
},
|
|
22
|
+
diagnostics: {
|
|
23
|
+
enabled: true,
|
|
24
|
+
consoleMode: 'summary',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
let packageLevelsEnvApplied = false;
|
|
28
|
+
function ensurePackageLogLevelsFromEnv() {
|
|
29
|
+
if (packageLevelsEnvApplied)
|
|
30
|
+
return;
|
|
31
|
+
applyPackageLogLevelsFromEnv();
|
|
32
|
+
packageLevelsEnvApplied = true;
|
|
33
|
+
}
|
|
34
|
+
function buildUserConfig(options) {
|
|
35
|
+
ensurePackageLogLevelsFromEnv();
|
|
36
|
+
const config = {
|
|
37
|
+
...DEFAULT_LOGXER_OPTIONS,
|
|
38
|
+
...(options.logging !== undefined ? { stack: options.logging } : {}),
|
|
39
|
+
};
|
|
40
|
+
if (options.level !== undefined) {
|
|
41
|
+
config.logLevel = options.level;
|
|
42
|
+
}
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
function createGateway(options = {}) {
|
|
46
|
+
return createLogxer(LOGXER_PACKAGE, buildUserConfig(options));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a logxer instance for this package (prefer over ad-hoc `createLogxer` in consumers).
|
|
50
|
+
*/
|
|
51
|
+
export function createRouterLogxer(options) {
|
|
52
|
+
return createGateway(options);
|
|
13
53
|
}
|
|
14
54
|
/**
|
|
15
55
|
* Logger class that wraps logxer with proper log levels
|
|
@@ -17,7 +57,8 @@ function createGateway(level) {
|
|
|
17
57
|
export class Logger {
|
|
18
58
|
constructor(config = {}) {
|
|
19
59
|
this.verbose = config.verbose ?? false;
|
|
20
|
-
this.
|
|
60
|
+
this.logging = config.logging;
|
|
61
|
+
this.gateway = createGateway({ level: config.level, logging: config.logging });
|
|
21
62
|
this.level = config.level ?? this.gateway.getConfig().logLevel;
|
|
22
63
|
}
|
|
23
64
|
/**
|
|
@@ -27,138 +68,109 @@ export class Logger {
|
|
|
27
68
|
if (config.verbose !== undefined) {
|
|
28
69
|
this.verbose = config.verbose;
|
|
29
70
|
}
|
|
30
|
-
if (config.
|
|
31
|
-
this.
|
|
32
|
-
|
|
71
|
+
if (config.logging !== undefined) {
|
|
72
|
+
this.logging = config.logging;
|
|
73
|
+
}
|
|
74
|
+
if (config.level !== undefined || config.logging !== undefined) {
|
|
75
|
+
if (config.level !== undefined) {
|
|
76
|
+
this.level = config.level;
|
|
77
|
+
}
|
|
78
|
+
this.gateway = createGateway({
|
|
79
|
+
level: config.level ?? this.level,
|
|
80
|
+
logging: config.logging ?? this.logging,
|
|
81
|
+
});
|
|
82
|
+
if (config.level === undefined) {
|
|
83
|
+
this.level = this.gateway.getConfig().logLevel;
|
|
84
|
+
}
|
|
33
85
|
}
|
|
34
86
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
87
|
+
getConfig() {
|
|
88
|
+
return this.gateway.getConfig();
|
|
89
|
+
}
|
|
90
|
+
isLevelEnabled(level) {
|
|
91
|
+
return this.gateway.isLevelEnabled(level);
|
|
92
|
+
}
|
|
93
|
+
infoCode(code, data) {
|
|
94
|
+
this.gateway.infoCode(code, data);
|
|
95
|
+
}
|
|
96
|
+
warnCode(code, data) {
|
|
97
|
+
this.gateway.warnCode(code, data);
|
|
98
|
+
}
|
|
99
|
+
errorCode(code, data) {
|
|
100
|
+
this.gateway.errorCode(code, data);
|
|
101
|
+
}
|
|
102
|
+
diagnostic(level, message, data) {
|
|
103
|
+
this.gateway.diagnostic(level, message, data);
|
|
104
|
+
}
|
|
105
|
+
success(message, data) {
|
|
106
|
+
this.gateway.success(message, data);
|
|
107
|
+
}
|
|
108
|
+
scopeLogs(criteria, options) {
|
|
109
|
+
return this.gateway.scopeLogs(criteria, options);
|
|
110
|
+
}
|
|
111
|
+
getJobLogs(input) {
|
|
112
|
+
return this.gateway.getJobLogs(input);
|
|
113
|
+
}
|
|
38
114
|
error(message, data) {
|
|
39
115
|
this.gateway.error(message, data ?? {});
|
|
40
116
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Log warning messages
|
|
43
|
-
*/
|
|
44
117
|
warn(message, data) {
|
|
45
118
|
this.gateway.warn(message, data ?? {});
|
|
46
119
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Log informational messages
|
|
49
|
-
*/
|
|
50
120
|
info(message, data) {
|
|
51
121
|
this.gateway.info(message, data ?? {});
|
|
52
122
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Log debug messages
|
|
55
|
-
*/
|
|
56
123
|
debug(message, data) {
|
|
57
124
|
this.gateway.debug(message, data ?? {});
|
|
58
125
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Log verbose messages (only when verbose mode is enabled)
|
|
61
|
-
*/
|
|
62
126
|
logVerbose(message, data) {
|
|
63
127
|
if (!this.verbose)
|
|
64
128
|
return;
|
|
65
129
|
this.gateway.verbose(message, data ?? {});
|
|
66
130
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Log AI request (unfiltered, only in verbose mode)
|
|
69
|
-
*/
|
|
70
131
|
logAIRequest(provider, request, metadata) {
|
|
71
132
|
if (!this.verbose)
|
|
72
133
|
return;
|
|
73
134
|
this.logVerbose('AI Request Sent', {
|
|
74
135
|
provider,
|
|
75
|
-
request
|
|
136
|
+
request,
|
|
76
137
|
metadata: metadata ?? {},
|
|
77
138
|
timestamp: new Date().toISOString(),
|
|
139
|
+
debugKind: DebugLogAbstract.STATE,
|
|
78
140
|
});
|
|
79
141
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Log AI response (unfiltered, only in verbose mode)
|
|
82
|
-
*/
|
|
83
142
|
logAIResponse(provider, response, metadata) {
|
|
84
143
|
if (!this.verbose)
|
|
85
144
|
return;
|
|
86
145
|
this.logVerbose('AI Response Received', {
|
|
87
146
|
provider,
|
|
88
|
-
response
|
|
147
|
+
response,
|
|
89
148
|
metadata: metadata ?? {},
|
|
90
149
|
timestamp: new Date().toISOString(),
|
|
150
|
+
debugKind: DebugLogAbstract.STATE,
|
|
91
151
|
});
|
|
92
152
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Log AI request/response pair (unfiltered, only in verbose mode)
|
|
95
|
-
*/
|
|
96
153
|
logAIIteraction(provider, request, response, duration, metadata) {
|
|
97
154
|
if (!this.verbose)
|
|
98
155
|
return;
|
|
99
156
|
this.logVerbose('AI Interaction Complete', {
|
|
100
157
|
provider,
|
|
101
|
-
request
|
|
102
|
-
response
|
|
158
|
+
request,
|
|
159
|
+
response,
|
|
103
160
|
duration: duration ? `${duration}ms` : undefined,
|
|
104
161
|
metadata: metadata ?? {},
|
|
105
162
|
timestamp: new Date().toISOString(),
|
|
163
|
+
debugKind: DebugLogAbstract.EVENT,
|
|
106
164
|
});
|
|
107
165
|
}
|
|
108
166
|
/**
|
|
109
|
-
*
|
|
167
|
+
* @deprecated Logxer sanitizes payloads on emit when `sanitization.enabled` is set (default for this logger).
|
|
110
168
|
*/
|
|
111
169
|
sanitizeForLogging(data) {
|
|
112
|
-
|
|
113
|
-
return data;
|
|
114
|
-
}
|
|
115
|
-
const seen = new WeakSet();
|
|
116
|
-
const sanitize = (obj, depth = 0) => {
|
|
117
|
-
if (depth > 10) {
|
|
118
|
-
return '[Max Depth Reached]';
|
|
119
|
-
}
|
|
120
|
-
if (obj === null || obj === undefined) {
|
|
121
|
-
return obj;
|
|
122
|
-
}
|
|
123
|
-
if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
|
|
124
|
-
return obj;
|
|
125
|
-
}
|
|
126
|
-
if (typeof obj === 'object') {
|
|
127
|
-
if (seen.has(obj)) {
|
|
128
|
-
return '[Circular Reference]';
|
|
129
|
-
}
|
|
130
|
-
seen.add(obj);
|
|
131
|
-
if (Array.isArray(obj)) {
|
|
132
|
-
return obj.map((item) => sanitize(item, depth + 1));
|
|
133
|
-
}
|
|
134
|
-
if (Buffer.isBuffer(obj)) {
|
|
135
|
-
return `[Buffer: ${obj.length} bytes]`;
|
|
136
|
-
}
|
|
137
|
-
const result = {};
|
|
138
|
-
for (const key in obj) {
|
|
139
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
140
|
-
if (key.toLowerCase().includes('password') ||
|
|
141
|
-
key.toLowerCase().includes('secret') ||
|
|
142
|
-
key.toLowerCase().includes('apikey') ||
|
|
143
|
-
(key.toLowerCase().includes('token') && key !== 'token')) {
|
|
144
|
-
result[key] = '[REDACTED]';
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
result[key] = sanitize(obj[key], depth + 1);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
return String(obj);
|
|
154
|
-
};
|
|
155
|
-
return sanitize(data);
|
|
170
|
+
return data;
|
|
156
171
|
}
|
|
157
172
|
}
|
|
158
173
|
let defaultLogger = null;
|
|
159
|
-
/**
|
|
160
|
-
* Get or create the default logger instance
|
|
161
|
-
*/
|
|
162
174
|
export function getLogger(config) {
|
|
163
175
|
if (!defaultLogger) {
|
|
164
176
|
defaultLogger = new Logger(config);
|
|
@@ -168,9 +180,6 @@ export function getLogger(config) {
|
|
|
168
180
|
}
|
|
169
181
|
return defaultLogger;
|
|
170
182
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Create a new logger instance
|
|
173
|
-
*/
|
|
174
183
|
export function createLogger(config = {}) {
|
|
175
184
|
return new Logger(config);
|
|
176
185
|
}
|
package/dist/router/Router.d.ts
CHANGED
|
@@ -15,7 +15,9 @@ export declare class AIRouter {
|
|
|
15
15
|
*/
|
|
16
16
|
private resolveProviderNameForCandidate;
|
|
17
17
|
/**
|
|
18
|
-
* Resolve provider name from request
|
|
18
|
+
* Resolve provider name from request.
|
|
19
|
+
* Prefers OpenRouter when USE_OPENROUTER is true (default); falls back to OpenRouter when
|
|
20
|
+
* the requested vendor has no direct provider registered but an OpenRouter key exists.
|
|
19
21
|
*/
|
|
20
22
|
private resolveProviderName;
|
|
21
23
|
/**
|
package/dist/router/Router.js
CHANGED
|
@@ -3,7 +3,7 @@ import { applyResponseNormalization } from '../normalization/applyResponseNormal
|
|
|
3
3
|
import { extractCostUsdFromRouterResponse } from '../normalization/cost.js';
|
|
4
4
|
import { FallbackExhaustedError } from '../errors.js';
|
|
5
5
|
import { buildCandidateRequest, buildFallbackCandidates, isRetryableError, summarizeError, toError, } from './fallbackUtils.js';
|
|
6
|
-
import { hasOpenRouterApiKey } from '../utils/openrouterEnv.js';
|
|
6
|
+
import { hasOpenRouterApiKey, shouldPreferOpenRouter } from '../utils/openrouterEnv.js';
|
|
7
7
|
import { attachPartialRouterPayload, buildPartialRouterPayload } from './partialErrorPayload.js';
|
|
8
8
|
/**
|
|
9
9
|
* Main router class
|
|
@@ -32,64 +32,62 @@ export class AIRouter {
|
|
|
32
32
|
return this.resolveProviderName(candidateInput);
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Resolve provider name from request
|
|
35
|
+
* Resolve provider name from request.
|
|
36
|
+
* Prefers OpenRouter when USE_OPENROUTER is true (default); falls back to OpenRouter when
|
|
37
|
+
* the requested vendor has no direct provider registered but an OpenRouter key exists.
|
|
36
38
|
*/
|
|
37
39
|
resolveProviderName(input) {
|
|
38
40
|
const hasOpenRouterAdapter = this.adapters.has('openrouter');
|
|
39
41
|
const hasOpenRouterProvider = this.providers.has('openrouter');
|
|
40
|
-
// Check if OpenRouter API key is set (OPENROUTER_API_KEY canonical; OPEN_ROUTER_KEY legacy)
|
|
41
42
|
const hasOpenRouterKey = hasOpenRouterApiKey();
|
|
42
|
-
|
|
43
|
+
const preferOpenRouter = shouldPreferOpenRouter(this.routerConfig);
|
|
44
|
+
const canUseOpenRouter = hasOpenRouterProvider || (hasOpenRouterAdapter && hasOpenRouterKey);
|
|
43
45
|
const cfgProviderRaw = input.request?.config?.provider;
|
|
44
46
|
const cfgProvider = typeof cfgProviderRaw === 'string' ? cfgProviderRaw.trim() : undefined;
|
|
45
47
|
const hasProviderInConfig = !!cfgProvider && cfgProvider !== '';
|
|
46
|
-
const
|
|
48
|
+
const topLevelProvider = typeof input.provider === 'string' && input.provider.trim() !== ''
|
|
49
|
+
? input.provider.trim()
|
|
50
|
+
: undefined;
|
|
47
51
|
const hasRegisteredProviders = this.providers.list().length > 0;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// This works even when router is created manually without calling createRouter()
|
|
53
|
-
// 4. Fallback: adapter exists + config.provider + no providers registered (auto-detect)
|
|
54
|
-
//
|
|
55
|
-
// IMPORTANT: When OpenRouter provider is registered OR OPEN_ROUTER_KEY is set, OpenRouter mode is active.
|
|
56
|
-
// All provider names (openai, grok, anthropic, etc.) route through OpenRouter.
|
|
57
|
-
// The interceptor sets input.provider = 'openrouter' when OpenRouter mode is enabled.
|
|
58
|
-
const isOpenRouterMode = hasProviderAtTopLevel ||
|
|
59
|
-
hasOpenRouterProvider || // OpenRouter provider registered = OpenRouter mode is DEFAULT
|
|
60
|
-
(hasOpenRouterAdapter && hasOpenRouterKey && hasProviderInConfig) || // AUTOMATIC: env var + adapter + config.provider
|
|
61
|
-
(hasOpenRouterAdapter && hasProviderInConfig && !hasRegisteredProviders); // Fallback: adapter + config.provider + no providers
|
|
62
|
-
if (isOpenRouterMode) {
|
|
52
|
+
if (topLevelProvider === 'openrouter' || cfgProvider === 'openrouter') {
|
|
53
|
+
return 'openrouter';
|
|
54
|
+
}
|
|
55
|
+
if (preferOpenRouter && canUseOpenRouter) {
|
|
63
56
|
return 'openrouter';
|
|
64
57
|
}
|
|
65
|
-
// Honor request.config.provider in normal flow
|
|
66
58
|
if (hasProviderInConfig) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (hasOpenRouterAdapter && !hasOpenRouterKey) {
|
|
72
|
-
throw new Error(`Provider '${cfgProvider}' specified in request.config.provider but not registered. ` +
|
|
73
|
-
`OpenRouter adapter is available - set OPENROUTER_API_KEY environment variable to enable automatic OpenRouter mode. ` +
|
|
74
|
-
`Available providers: ${available || '(none)'}`);
|
|
59
|
+
if (this.providers.has(cfgProvider)) {
|
|
60
|
+
if (!this.adapters.has(cfgProvider)) {
|
|
61
|
+
const available = this.adapters.list().join(', ');
|
|
62
|
+
throw new Error(`Adapter '${cfgProvider}' specified in request.config.provider but not registered. Available adapters: ${available || '(none)'}`);
|
|
75
63
|
}
|
|
76
|
-
|
|
64
|
+
return cfgProvider;
|
|
65
|
+
}
|
|
66
|
+
if (canUseOpenRouter) {
|
|
67
|
+
return 'openrouter';
|
|
68
|
+
}
|
|
69
|
+
const available = this.providers.list().join(', ');
|
|
70
|
+
if (hasOpenRouterAdapter && !hasOpenRouterKey) {
|
|
71
|
+
throw new Error(`Provider '${cfgProvider}' specified in request.config.provider but not registered. ` +
|
|
72
|
+
`OpenRouter adapter is available - set OPENROUTER_API_KEY environment variable to enable OpenRouter fallback. ` +
|
|
73
|
+
`Available providers: ${available || '(none)'}`);
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Provider '${cfgProvider}' specified in request.config.provider but not registered. Available providers: ${available || '(none)'}`);
|
|
76
|
+
}
|
|
77
|
+
if (topLevelProvider) {
|
|
78
|
+
if (this.providers.has(topLevelProvider)) {
|
|
79
|
+
return topLevelProvider;
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const available = this.adapters.list().join(', ');
|
|
81
|
-
throw new Error(`Adapter '${cfgProvider}' specified in request.config.provider but not registered. Available adapters: ${available || '(none)'}`);
|
|
81
|
+
if (canUseOpenRouter) {
|
|
82
|
+
return 'openrouter';
|
|
82
83
|
}
|
|
83
|
-
return cfgProvider;
|
|
84
84
|
}
|
|
85
|
-
// Existing fallback: top-level provider or first registered provider
|
|
86
85
|
const providerName = input.provider ?? this.providers.list()[0];
|
|
87
86
|
if (!providerName) {
|
|
88
|
-
// Improved error messages with OpenRouter mode guidance
|
|
89
87
|
if (hasProviderInConfig && hasOpenRouterAdapter && !hasRegisteredProviders) {
|
|
90
88
|
if (!hasOpenRouterKey) {
|
|
91
89
|
throw new Error('OpenRouter adapter available and config.provider specified, but OpenRouter provider module not registered. ' +
|
|
92
|
-
'Set OPENROUTER_API_KEY environment variable to enable
|
|
90
|
+
'Set OPENROUTER_API_KEY environment variable to enable OpenRouter fallback (works with both createRouter() and manual initialization).');
|
|
93
91
|
}
|
|
94
92
|
throw new Error('OpenRouter adapter available and config.provider specified, but OpenRouter provider module not registered. ' +
|
|
95
93
|
'OPENROUTER_API_KEY is set but provider module initialization failed. Check that @x12i/ai-provider-openai is installed.');
|
|
@@ -97,10 +95,9 @@ export class AIRouter {
|
|
|
97
95
|
if (hasProviderInConfig && !hasOpenRouterAdapter) {
|
|
98
96
|
throw new Error(`Provider '${input.request?.config?.provider}' specified in config but no providers registered and OpenRouter adapter not available.`);
|
|
99
97
|
}
|
|
100
|
-
// Final fallback error with OpenRouter suggestion
|
|
101
98
|
if (hasOpenRouterAdapter && !hasOpenRouterKey) {
|
|
102
99
|
throw new Error('No provider specified and no providers registered. ' +
|
|
103
|
-
'OpenRouter adapter is available - set OPENROUTER_API_KEY environment variable to enable
|
|
100
|
+
'OpenRouter adapter is available - set OPENROUTER_API_KEY environment variable to enable OpenRouter fallback.');
|
|
104
101
|
}
|
|
105
102
|
throw new Error('No provider specified and no providers registered');
|
|
106
103
|
}
|
|
@@ -72,6 +72,8 @@ export type DiagnosticsMetadata = {
|
|
|
72
72
|
/** Allow additional vendor/provider specific keys. */
|
|
73
73
|
[key: string]: unknown;
|
|
74
74
|
};
|
|
75
|
+
import type { Logger } from '../logger.js';
|
|
76
|
+
import type { LogLevel, StackLoggingOptions } from '@x12i/logxer';
|
|
75
77
|
/**
|
|
76
78
|
* Router configuration
|
|
77
79
|
*/
|
|
@@ -91,10 +93,21 @@ export interface RouterConfig {
|
|
|
91
93
|
}): void;
|
|
92
94
|
};
|
|
93
95
|
verbose?: boolean;
|
|
94
|
-
logLevel?:
|
|
95
|
-
|
|
96
|
+
logLevel?: LogLevel;
|
|
97
|
+
/**
|
|
98
|
+
* Host-app pass-through (logxer ≥ 4.5). Affects **only** this package's logger
|
|
99
|
+
* (`AI_PROVIDER_ROUTER` via `stack`). Provider packages under the router do not use logxer 4.5.
|
|
100
|
+
*/
|
|
101
|
+
logging?: StackLoggingOptions;
|
|
102
|
+
logger?: Logger;
|
|
96
103
|
timeoutMs?: number;
|
|
97
104
|
defaultTimeoutMs?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Prefer OpenRouter for vendor calls when OPENROUTER_API_KEY is set (default: true).
|
|
107
|
+
* Maps to USE_OPENROUTER env. When false, direct providers are used when keys exist;
|
|
108
|
+
* OpenRouter is still used as fallback for providers without a direct key.
|
|
109
|
+
*/
|
|
110
|
+
useOpenRouter?: boolean;
|
|
98
111
|
/** OpenRouter provider config (e.g. from gateway); used when env OPEN_ROUTER_KEY is not visible */
|
|
99
112
|
openrouter?: {
|
|
100
113
|
apiKey?: string;
|
|
@@ -4,8 +4,8 @@ import { AdapterRegistry } from '../registry/AdapterRegistry.js';
|
|
|
4
4
|
import { OpenAIAdapter } from '../adapters/openai/OpenAIAdapter.js';
|
|
5
5
|
import { GrokAdapter } from '../adapters/grok/GrokAdapter.js';
|
|
6
6
|
import { OpenRouterAdapter } from '../adapters/openrouter/OpenRouterAdapter.js';
|
|
7
|
-
import { getLogger } from '../logger.js';
|
|
8
|
-
import { resolveOpenRouterApiKey, OPENROUTER_API_KEY_ERC } from '../utils/openrouterEnv.js';
|
|
7
|
+
import { getLogger, runWithLogContext, DebugLogAbstract } from '../logger.js';
|
|
8
|
+
import { resolveOpenRouterApiKey, OPENROUTER_API_KEY_ERC, shouldPreferOpenRouter } from '../utils/openrouterEnv.js';
|
|
9
9
|
import { resolveModelVendorSlug } from '../utils/openrouterModelVendor.js';
|
|
10
10
|
/**
|
|
11
11
|
* Resolve ENV. placeholders to actual environment variable values
|
|
@@ -52,7 +52,12 @@ export class LLMProviderRouter {
|
|
|
52
52
|
// Initialize logger
|
|
53
53
|
this.logger = config?.logger || getLogger({
|
|
54
54
|
verbose: config?.verbose || false,
|
|
55
|
-
|
|
55
|
+
logging: config?.logging,
|
|
56
|
+
...(config?.logLevel !== undefined
|
|
57
|
+
? { level: config.logLevel }
|
|
58
|
+
: config?.logging
|
|
59
|
+
? {}
|
|
60
|
+
: { level: 'info' }),
|
|
56
61
|
});
|
|
57
62
|
// Initialize registries
|
|
58
63
|
this.providerRegistry = new ProviderRegistry();
|
|
@@ -66,6 +71,7 @@ export class LLMProviderRouter {
|
|
|
66
71
|
this.logger.info('Router initialized with ProviderModule architecture', {
|
|
67
72
|
verbose: this.logger.verbose,
|
|
68
73
|
logLevel: this.logger.level,
|
|
74
|
+
debugKind: DebugLogAbstract.EVENT,
|
|
69
75
|
});
|
|
70
76
|
}
|
|
71
77
|
/**
|
|
@@ -172,6 +178,9 @@ export class LLMProviderRouter {
|
|
|
172
178
|
* Invoke router (sync mode)
|
|
173
179
|
*/
|
|
174
180
|
async invoke(request) {
|
|
181
|
+
return runWithLogContext({ correlationId: request.requestId }, () => this.invokeWithLogContext(request));
|
|
182
|
+
}
|
|
183
|
+
async invokeWithLogContext(request) {
|
|
175
184
|
const startTime = Date.now();
|
|
176
185
|
// Auto-register providers if needed
|
|
177
186
|
await this.ensureProvidersRegistered();
|
|
@@ -190,6 +199,7 @@ export class LLMProviderRouter {
|
|
|
190
199
|
// Log request
|
|
191
200
|
this.logger.logAIRequest(request.provider || 'unknown', processedRequest, {
|
|
192
201
|
requestId: processedRequest.requestId,
|
|
202
|
+
correlationId: request.requestId,
|
|
193
203
|
});
|
|
194
204
|
// Execute
|
|
195
205
|
const invokeStartTime = Date.now();
|
|
@@ -216,6 +226,8 @@ export class LLMProviderRouter {
|
|
|
216
226
|
provider: request.provider,
|
|
217
227
|
error: err.message,
|
|
218
228
|
stack: err.stack,
|
|
229
|
+
requestId: processedRequest.requestId,
|
|
230
|
+
debugKind: DebugLogAbstract.ANOMALY,
|
|
219
231
|
});
|
|
220
232
|
throw err;
|
|
221
233
|
}
|
|
@@ -272,6 +284,7 @@ export class LLMProviderRouter {
|
|
|
272
284
|
// Log request
|
|
273
285
|
this.logger.logAIRequest(request.provider || 'unknown', processedRequest, {
|
|
274
286
|
requestId: processedRequest.requestId,
|
|
287
|
+
correlationId: request.requestId,
|
|
275
288
|
});
|
|
276
289
|
// Stream
|
|
277
290
|
for await (const event of this.router.runStream(processedRequest)) {
|
|
@@ -407,6 +420,9 @@ export class LLMProviderRouter {
|
|
|
407
420
|
if (providerModule) {
|
|
408
421
|
this.providerRegistry.register(providerModule);
|
|
409
422
|
this.addRequestInterceptor(async (req, originalProvider) => {
|
|
423
|
+
if (!shouldPreferOpenRouter(this.config)) {
|
|
424
|
+
return req;
|
|
425
|
+
}
|
|
410
426
|
const newRequest = { ...req };
|
|
411
427
|
if (!newRequest.request)
|
|
412
428
|
newRequest.request = {};
|
|
@@ -445,7 +461,9 @@ export class LLMProviderRouter {
|
|
|
445
461
|
newRequest.provider = 'openrouter';
|
|
446
462
|
return newRequest;
|
|
447
463
|
});
|
|
448
|
-
this.logger.info('Auto-registered OpenRouter provider
|
|
464
|
+
this.logger.info('Auto-registered OpenRouter provider', {
|
|
465
|
+
preferOpenRouter: shouldPreferOpenRouter(this.config),
|
|
466
|
+
});
|
|
449
467
|
}
|
|
450
468
|
}
|
|
451
469
|
catch (e) {
|
|
@@ -494,10 +512,9 @@ export class LLMProviderRouter {
|
|
|
494
512
|
config.openrouter.apiKey = resolveOpenRouterApiKey();
|
|
495
513
|
}
|
|
496
514
|
await this.tryRegisterOpenRouter(config);
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
this.logger.info('OpenRouter mode active - skipping individual providers to avoid state conflicts');
|
|
515
|
+
// When OpenRouter is preferred (default), skip direct providers to avoid singleton conflicts.
|
|
516
|
+
if (this.providerRegistry.has('openrouter') && shouldPreferOpenRouter(this.config)) {
|
|
517
|
+
this.logger.info('OpenRouter preferred - skipping individual provider auto-registration');
|
|
501
518
|
return;
|
|
502
519
|
}
|
|
503
520
|
// 2. OpenAI
|
|
@@ -6,3 +6,21 @@ export declare function resolveOpenRouterApiKey(): string | undefined;
|
|
|
6
6
|
export declare function hasOpenRouterApiKey(): boolean;
|
|
7
7
|
/** ERC placeholder resolution: prefer canonical name, fall back to legacy. */
|
|
8
8
|
export declare const OPENROUTER_API_KEY_ERC = "ENV.OPENROUTER_API_KEY||ENV.OPEN_ROUTER_KEY";
|
|
9
|
+
export interface ResolveUseOpenRouterOptions {
|
|
10
|
+
/** Programmatic override from RouterConfig.useOpenRouter — wins over environment. */
|
|
11
|
+
userValue?: boolean;
|
|
12
|
+
/** For tests; defaults to `process.env`. */
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether to prefer OpenRouter for vendor calls when an OpenRouter key is available.
|
|
17
|
+
*
|
|
18
|
+
* Default: `true` (route through OpenRouter even when direct provider keys exist).
|
|
19
|
+
* Set `USE_OPENROUTER=false` to use direct providers when keys exist; OpenRouter remains
|
|
20
|
+
* available as fallback for providers without a direct key.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveUseOpenRouter(options?: ResolveUseOpenRouterOptions): boolean;
|
|
23
|
+
/** True when OpenRouter should be the primary transport (not merely fallback). */
|
|
24
|
+
export declare function shouldPreferOpenRouter(config: {
|
|
25
|
+
useOpenRouter?: boolean;
|
|
26
|
+
} | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -16,3 +16,31 @@ export function hasOpenRouterApiKey() {
|
|
|
16
16
|
}
|
|
17
17
|
/** ERC placeholder resolution: prefer canonical name, fall back to legacy. */
|
|
18
18
|
export const OPENROUTER_API_KEY_ERC = 'ENV.OPENROUTER_API_KEY||ENV.OPEN_ROUTER_KEY';
|
|
19
|
+
/**
|
|
20
|
+
* Whether to prefer OpenRouter for vendor calls when an OpenRouter key is available.
|
|
21
|
+
*
|
|
22
|
+
* Default: `true` (route through OpenRouter even when direct provider keys exist).
|
|
23
|
+
* Set `USE_OPENROUTER=false` to use direct providers when keys exist; OpenRouter remains
|
|
24
|
+
* available as fallback for providers without a direct key.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveUseOpenRouter(options = {}) {
|
|
27
|
+
const { userValue, env = process.env } = options;
|
|
28
|
+
if (userValue === false)
|
|
29
|
+
return false;
|
|
30
|
+
if (userValue === true)
|
|
31
|
+
return true;
|
|
32
|
+
const raw = env.USE_OPENROUTER;
|
|
33
|
+
if (raw === undefined || raw === '')
|
|
34
|
+
return true;
|
|
35
|
+
if (raw === 'false')
|
|
36
|
+
return false;
|
|
37
|
+
if (raw === 'true')
|
|
38
|
+
return true;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** True when OpenRouter should be the primary transport (not merely fallback). */
|
|
42
|
+
export function shouldPreferOpenRouter(config, env) {
|
|
43
|
+
if (!hasOpenRouterApiKey())
|
|
44
|
+
return false;
|
|
45
|
+
return resolveUseOpenRouter({ userValue: config?.useOpenRouter, env });
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x12i/ai-providers-router",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.8",
|
|
4
4
|
"description": "Unified router for all LLM provider implementations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@x12i/ai-provider-grok": "^3.2.0",
|
|
47
47
|
"@x12i/ai-provider-interface": "^3.2.1",
|
|
48
48
|
"@x12i/ai-provider-openai": "^3.2.1",
|
|
49
|
-
"@x12i/logxer": "^4.
|
|
49
|
+
"@x12i/logxer": "^4.5.0",
|
|
50
50
|
"ai-io-normalizer": "^6.0.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|