@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 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 OpenRouter mode is active |
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. When enabled, calls can use familiar provider names (`openai`, `grok`, `anthropic`, …) while traffic routes through OpenRouter with automatic model mapping (e.g. `openai` + `gpt-4o` → `openai/gpt-4o`).
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
- OpenRouter mode activates automatically when a valid key is present. To disable explicitly:
160
+ Optional ranking headers:
161
161
 
162
162
  ```bash
163
- export USE_OPENROUTER=false
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
- Optional ranking headers:
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 OPENROUTER_HTTP_REFERER=https://your-site.com # legacy: OPEN_ROUTER_HTTP_REFERER
170
- export OPENROUTER_X_TITLE=Your Site Name # legacy: OPEN_ROUTER_X_TITLE
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
- ### Behavior
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 routing internally
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
- - When OpenRouter mode is active, direct provider packages are **not** auto-registered (avoids singleton config conflicts)
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` | — | Canonical OpenRouter key |
282
- | `OPEN_ROUTER_KEY` | — | Legacy alias |
283
- | `USE_OPENROUTER` | auto | `true` when key present; set `false` to disable |
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 and no programmatic `logLevel` is passed, logxer defaults to `warn`. The router's `createRouter()` zero-config path resolves `AI_PROVIDER_ROUTER_LOG_LEVEL` with default `info`.
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
- **Programmatic:**
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 { createRouter, createLogger, getLogger } from '@x12i/ai-providers-router';
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 router = await createRouter({ logLevel: 'debug', verbose: true });
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
- // Or inject a custom logger instance
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 (passwords, secrets, and API keys are redacted). Cross-cutting log sinks (console, file, format) are configured at the host/app level via logxer — not per-package.
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 mode registers a request interceptor automatically to route vendor calls through OpenRouter while preserving the original provider name for model mapping.
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 also **auto-registered on first invoke** when matching API keys are in the environment (unless OpenRouter mode is active).
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 finalConfig = Object.keys(config || {}).length > 0 ? config : {
135
- logLevel: ercResult.config.router.logLevel,
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
- * Provides structured logging with proper log levels and verbose mode support
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
- * Log error messages
25
- */
26
- error(message: string, data?: Record<string, unknown>): void;
27
- /**
28
- * Log warning messages
29
- */
30
- warn(message: string, data?: Record<string, unknown>): void;
31
- /**
32
- * Log informational messages
33
- */
34
- info(message: string, data?: Record<string, unknown>): void;
35
- /**
36
- * Log debug messages
37
- */
38
- debug(message: string, data?: Record<string, unknown>): void;
39
- /**
40
- * Log verbose messages (only when verbose mode is enabled)
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
- * Provides structured logging with proper log levels and verbose mode support
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: 'AI_PROVIDER_ROUTER',
11
+ envPrefix: ROUTER_LOG_ENV_PREFIX,
9
12
  debugNamespace: 'ai-providers-router',
10
13
  };
11
- function createGateway(level) {
12
- return createLogxer(LOGXER_PACKAGE, level !== undefined ? { logLevel: level } : undefined);
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.gateway = createGateway(config.level);
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.level !== undefined) {
31
- this.level = config.level;
32
- this.gateway = createGateway(config.level);
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
- * Log error messages
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: this.sanitizeForLogging(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: this.sanitizeForLogging(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: this.sanitizeForLogging(request),
102
- response: this.sanitizeForLogging(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
- * Sanitize data for logging (remove sensitive info, handle circular refs)
167
+ * @deprecated Logxer sanitizes payloads on emit when `sanitization.enabled` is set (default for this logger).
110
168
  */
111
169
  sanitizeForLogging(data) {
112
- if (data === null || data === undefined) {
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
  }
@@ -15,7 +15,9 @@ export declare class AIRouter {
15
15
  */
16
16
  private resolveProviderNameForCandidate;
17
17
  /**
18
- * Resolve provider name from request, checking OpenRouter mode first
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
  /**
@@ -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, checking OpenRouter mode first
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
- // Normalize config.provider (handle string, trim whitespace)
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 hasProviderAtTopLevel = input.provider === 'openrouter';
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
- // OpenRouter mode detection (COMPLETELY AUTOMATIC):
49
- // 1. Explicit: top-level provider is 'openrouter'
50
- // 2. OpenRouter provider is registered (when OPEN_ROUTER_KEY is set via factory)
51
- // 3. AUTOMATIC: OpenRouter adapter exists + OPEN_ROUTER_KEY env var is set + config.provider is specified
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
- // Validate provider is registered
68
- if (!this.providers.has(cfgProvider)) {
69
- const available = this.providers.list().join(', ');
70
- // Check if OpenRouter mode could work automatically
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
- throw new Error(`Provider '${cfgProvider}' specified in request.config.provider but not registered. Available providers: ${available || '(none)'}`);
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
- // Validate adapter is registered
79
- if (!this.adapters.has(cfgProvider)) {
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 automatic OpenRouter mode (works with both createRouter() and manual initialization).');
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 automatic OpenRouter mode.');
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?: 'error' | 'warn' | 'info' | 'debug' | 'verbose';
95
- logger?: any;
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;
@@ -39,6 +39,7 @@ export declare class LLMProviderRouter {
39
39
  * Invoke router (sync mode)
40
40
  */
41
41
  invoke(request: AIRouterRequest): Promise<AIResponse>;
42
+ private invokeWithLogContext;
42
43
  /**
43
44
  * Stream request
44
45
  */
@@ -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
- level: config?.logLevel || 'info',
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 and enabled OpenRouter mode');
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
- // If OpenRouter is being used, we SKIP auto-registering other providers
498
- // to avoid global state conflicts (e.g. ai-provider-openai singleton config).
499
- if (this.providerRegistry.has('openrouter')) {
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.6",
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.4.0",
49
+ "@x12i/logxer": "^4.5.0",
50
50
  "ai-io-normalizer": "^6.0.3"
51
51
  },
52
52
  "devDependencies": {