@x12i/ai-providers-router 4.8.5 → 4.8.6

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
@@ -1,24 +1,50 @@
1
1
  # @x12i/ai-providers-router
2
2
 
3
- A unified **LLM provider router** that routes requests to installed provider packages using the **ProviderModule architecture**.
3
+ Unified **LLM provider router** for Node.js. Routes requests to installed provider packages using the **ProviderModule** architecture from [`@x12i/ai-provider-interface`](https://www.npmjs.com/package/@x12i/ai-provider-interface).
4
4
 
5
- This router:
6
- - **OpenRouter Mode**: Access 353+ models from 67 providers using catalog-driven routing
7
- - Chooses a provider/model (and optionally a fallback chain)
8
- - Loads ProviderModules from installed provider packages (lazy import)
9
- - Uses router-side adapters to convert requests to ProviderSDKCallSpec
10
- - Executes via ProviderModule.execute() / stream() / submitBatch()
11
- - Parses responses using router-side adapters
12
- - Returns standardized responses with lossless rawResponse
5
+ **Highlights**
13
6
 
14
- ## Architecture
7
+ - **Multi-provider routing** — OpenAI, Grok, and more via lazy-loaded provider packages
8
+ - **OpenRouter mode** — Access 350+ models from 60+ providers through one API key
9
+ - **Sync, stream, and batch** — Gated by each provider's declared capabilities
10
+ - **Fallback chains** — Automatic provider/model failover with full attempt traces
11
+ - **Structured diagnostics** — Usage, cost, timing, and ordered `metadata.attempts[]`
12
+ - **Reasoning support** — Cross-vendor effort, visibility, and encrypted trace handling
13
+ - **ERC 2.0** — Zero-config initialization from environment variables
14
+ - **Structured logging** — Powered by [`@x12i/logxer`](https://www.npmjs.com/package/@x12i/logxer)
15
+
16
+ > This router **never installs** provider packages at runtime. You must install the packages you intend to use.
15
17
 
16
- - **ProviderModule**: Provider packages export ProviderModules that implement `@x12i/ai-provider-interface`
17
- - **Router Adapters**: Router-side adapters convert router requests to ProviderSDKCallSpec and parse responses
18
- - **Capability Gating**: Router gates execution by `provider.capabilities.modes.sync/stream/batch` (ProviderModule is source of truth)
19
- - **Execution Semantics**: Router owns execution semantics (timeoutMs, retries, idempotencyKey, signal)
18
+ ---
20
19
 
21
- > Important: This router **never installs** provider packages at runtime.
20
+ ## Table of contents
21
+
22
+ - [Install](#install)
23
+ - [Quick start](#quick-start)
24
+ - [Architecture](#architecture)
25
+ - [Provider IDs](#provider-ids)
26
+ - [OpenRouter mode](#openrouter-mode)
27
+ - [Configuration](#configuration)
28
+ - [Logging](#logging)
29
+ - [API usage](#api-usage)
30
+ - [Sync](#sync-call)
31
+ - [Streaming](#streaming-call)
32
+ - [Batch](#batch-requests)
33
+ - [Request and response types](#request-and-response-types)
34
+ - [Trace diagnostics](#trace-diagnostics)
35
+ - [Reasoning](#reasoning)
36
+ - [Fallback chains](#fallback-chains)
37
+ - [Interceptors](#interceptors)
38
+ - [Health checks](#health-checks)
39
+ - [AIGateway](#aigateway)
40
+ - [Response normalization and cost](#response-normalization-and-cost)
41
+ - [Error types](#error-types)
42
+ - [Manual setup (advanced)](#manual-setup-advanced)
43
+ - [Public API exports](#public-api-exports)
44
+ - [Provider packages](#provider-packages)
45
+ - [Development and testing](#development-and-testing)
46
+ - [Related documentation](#related-documentation)
47
+ - [License](#license)
22
48
 
23
49
  ---
24
50
 
@@ -28,470 +54,634 @@ This router:
28
54
  npm i @x12i/ai-providers-router
29
55
  ```
30
56
 
31
- Install at least one provider package (examples):
57
+ **Bundled provider packages** (included as dependencies):
58
+
59
+ - `@x12i/ai-provider-openai` — OpenAI and OpenRouter-compatible APIs
60
+ - `@x12i/ai-provider-grok` — Grok / xAI
61
+
62
+ **Optional provider packages** (install when you need direct access):
32
63
 
33
64
  ```bash
34
- npm i @x12i/ai-provider-openai
35
65
  npm i @x12i/ai-provider-anthropic
36
66
  npm i @x12i/ai-provider-google
37
- npm i @x12i/ai-provider-xai
38
67
  npm i @x12i/ai-provider-groq
68
+ # ... other @x12i/ai-provider-* packages
39
69
  ```
40
70
 
41
- **For OpenRouter mode**: Only `@x12i/ai-provider-openai` is required to access **353 models from 67 providers** through OpenRouter's unified API.
71
+ For **OpenRouter mode**, only `@x12i/ai-provider-openai` is required to reach models from many vendors through OpenRouter's unified API.
42
72
 
43
73
  ---
44
74
 
45
- ## Provider IDs (canonical)
75
+ ## Quick start
76
+
77
+ ```ts
78
+ import { createRouter, type AIRouterRequest, type AIResponse } from '@x12i/ai-providers-router';
79
+
80
+ const router = await createRouter();
46
81
 
47
- **Core Providers:**
48
- * `openai` OpenAI
49
- * `anthropic` Claude
50
- * `google` Gemini
51
- * `xai` → Grok (xAI)
52
- * `groq` → GroqCloud (Llama/Mixtral/OSS models)
53
- * `kimi` → Moonshot/Kimi (if installed)
82
+ const req: AIRouterRequest = {
83
+ request: {
84
+ messages: [{ role: 'user', content: 'Write 3 bullets about routers.' }],
85
+ config: { model: 'gpt-4o-mini', maxTokens: 200 },
86
+ },
87
+ provider: 'openai',
88
+ mode: 'sync',
89
+ };
54
90
 
55
- **OpenRouter Mode (67 providers supported):**
56
- * `openrouter` → OpenRouter (unified gateway to all providers)
57
- * All provider names work seamlessly (automatic routing through OpenRouter)
58
- * Access to 353+ models from providers like Meta, Mistral, Cohere, Perplexity, and many more
91
+ const res: AIResponse = await router.invoke(req);
92
+ console.log(res.outputText);
93
+ console.log(res.usage);
94
+ console.log(res.rawResponse); // always present lossless provider payload
95
+ ```
59
96
 
60
- > Grok Groq
61
- >
62
- > * Grok is **xAI** (`xai`)
63
- > * Groq is **GroqCloud** (`groq`)
97
+ Set provider API keys in your environment (see [Configuration](#configuration)). With no arguments, `createRouter()` auto-discovers settings via ERC.
64
98
 
65
99
  ---
66
100
 
67
- ## OpenRouter Mode
101
+ ## Architecture
68
102
 
69
- OpenRouter is a unified API gateway that provides access to multiple AI models from different providers. When OpenRouter mode is enabled, **all provider calls automatically route through OpenRouter** while maintaining a seamless API experience.
103
+ ```
104
+ AIRouterRequest
105
+ → request interceptors (e.g. OpenRouter routing)
106
+ → ProviderModule (from installed @x12i/ai-provider-* package)
107
+ → router-side adapter (request → ProviderSDKCallSpec)
108
+ → provider.execute() | stream() | submitBatch()
109
+ → router-side adapter (ProviderSDKExecResult → AIResponse)
110
+ → response interceptors
111
+ → AIResponse (with lossless rawResponse)
112
+ ```
70
113
 
71
- ### Key Features
114
+ | Layer | Role |
115
+ |-------|------|
116
+ | **ProviderModule** | Provider packages implement `@x12i/ai-provider-interface` |
117
+ | **Router adapters** | Convert router requests to `ProviderSDKCallSpec` and parse responses |
118
+ | **Capability gating** | Router checks `provider.capabilities.modes.sync/stream/batch` |
119
+ | **Execution semantics** | Router owns `timeoutMs`, retries, `idempotencyKey`, `AbortSignal` |
72
120
 
73
- - **Comprehensive Model Catalog**: Access **353 models** from **67 providers** using catalog data automatically loaded from OpenRouter APIs
74
- - **Seamless API**: Use the same provider names (`"openai"`, `"grok"`, `"anthropic"`, etc.) - no code changes needed
75
- - **Smart Provider Inference**: Uses catalog data to automatically infer providers from model names (e.g., `"gpt-4o"` → `"openai"`)
76
- - **Model Validation**: Validates models against available OpenRouter catalog and warns about invalid models
77
- - **Provider Aliases**: Supports vendor mappings (e.g., `xai` models route to `grok` provider)
78
- - **Model Name Mapping**: Automatically converts provider + model to OpenRouter format (e.g., `provider: "openai"` + `model: "gpt-4o"` → `"openai/gpt-4o"`)
79
- - **Access any OpenRouter model**: Call models even without direct provider packages (e.g., `"meta-llama/llama-3-70b-instruct"`)
80
- - **Unified Reasoning API**: Cross-vendor reasoning support with effort control and visibility options (see [Reasoning Integration](./docs/reasoning-integration.md))
81
- - **No ai-io-normalizer**: OpenRouter responses are parsed directly (faster, simpler)
121
+ ---
82
122
 
83
- ### OpenRouter Mode - Completely Automatic
123
+ ## Provider IDs
84
124
 
85
- **OpenRouter mode works automatically - no code changes required!**
125
+ **Direct providers** (require matching `@x12i/ai-provider-*` package and API key):
86
126
 
87
- Simply set the `OPEN_ROUTER_KEY` environment variable:
127
+ | ID | Vendor |
128
+ |----|--------|
129
+ | `openai` | OpenAI |
130
+ | `grok` | Grok / xAI |
131
+ | `anthropic` | Claude |
132
+ | `google` | Gemini |
133
+ | `groq` | GroqCloud |
88
134
 
89
- ```bash
90
- export OPEN_ROUTER_KEY=sk-or-your-openrouter-api-key-here
91
- ```
135
+ **OpenRouter** (unified gateway):
92
136
 
93
- That's it! OpenRouter mode is **completely automatic** and works with:
137
+ | ID | Role |
138
+ |----|------|
139
+ | `openrouter` | Explicit OpenRouter transport |
140
+ | Any vendor ID | Routed through OpenRouter when OpenRouter mode is active |
94
141
 
95
- - **Factory initialization**: `await createRouter()` - automatically registers OpenRouter provider module
96
- - ✅ **Manual initialization**: `new LLMProviderRouter()` - automatically detects OpenRouter mode via environment variable
97
- - ✅ **Any provider name**: Use `config.provider: "openai"`, `"grok"`, `"anthropic"`, etc. - all route through OpenRouter automatically
142
+ > **Grok ≠ Groq** Grok is xAI (`grok` / `xai`). Groq is GroqCloud (`groq`).
98
143
 
99
- **How it works:**
100
- - When `OPEN_ROUTER_KEY` is set, the router automatically detects OpenRouter mode
101
- - All provider requests (openai, grok, anthropic, etc.) automatically route through OpenRouter
102
- - No need to register individual provider modules - OpenRouter handles everything
103
- - Works seamlessly whether you use `createRouter()` or manual `new LLMProviderRouter()` initialization
144
+ ---
145
+
146
+ ## OpenRouter mode
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`).
149
+
150
+ ### Enable OpenRouter
151
+
152
+ Set an API key (canonical name preferred):
153
+
154
+ ```bash
155
+ export OPENROUTER_API_KEY=sk-or-your-key-here
156
+ # Legacy alias also supported:
157
+ # export OPEN_ROUTER_KEY=sk-or-your-key-here
158
+ ```
104
159
 
105
- **To disable OpenRouter mode explicitly:**
160
+ OpenRouter mode activates automatically when a valid key is present. To disable explicitly:
106
161
 
107
162
  ```bash
108
163
  export USE_OPENROUTER=false
109
164
  ```
110
165
 
111
- **Note**: When OpenRouter mode is enabled, direct provider packages are not registered to avoid conflicts. All calls route through OpenRouter using the integrated catalog data (`.metadata/openrouter_catalog_with_vendor_mapping.json`).
166
+ Optional ranking headers:
112
167
 
113
- **Troubleshooting:**
168
+ ```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
171
+ ```
114
172
 
115
- If you see errors like "No provider specified and no providers registered":
116
- 1. ✅ Check that `OPEN_ROUTER_KEY` is set: `echo $OPEN_ROUTER_KEY`
117
- 2. ✅ Verify the key is valid (not empty, doesn't start with "ENV.")
118
- 3. ✅ Ensure `config.provider` is specified in your request (e.g., `config: { provider: "openai", model: "gpt-4o" }`)
119
- 4. ✅ The OpenRouter adapter is always registered - no additional setup needed
173
+ ### Behavior
120
174
 
121
- The router will automatically use OpenRouter mode when these conditions are met!
175
+ - 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
177
+ - 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)
122
180
 
123
- ### Usage Examples
181
+ ### Examples
124
182
 
125
- **Example 1: Using provider names (seamless - no code changes needed):**
183
+ **Same provider name, OpenRouter underneath:**
126
184
 
127
185
  ```ts
128
- const router = await createRouter();
129
-
130
- // Works exactly the same whether OpenRouter mode is on or off
131
186
  const req: AIRouterRequest = {
132
- request: {
133
- messages: [{ role: 'user', content: 'Hello!' }],
134
- config: { model: 'gpt-4o' },
135
- },
136
- provider: 'openai', // Still use "openai" - router handles routing
187
+ request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'gpt-4o' } },
188
+ provider: 'openai',
137
189
  mode: 'sync',
138
190
  };
139
-
140
- const res = await router.invoke(req);
141
- // Model automatically mapped to "openai/gpt-4o" when using OpenRouter
191
+ await router.invoke(req);
142
192
  ```
143
193
 
144
- **Example 2: Provider inference (no provider specified):**
194
+ **OpenRouter model format directly:**
145
195
 
146
196
  ```ts
147
- // Router infers provider from model name
148
197
  const req: AIRouterRequest = {
149
198
  request: {
150
199
  messages: [{ role: 'user', content: 'Hello!' }],
151
- config: { model: 'gpt-4o' }, // Infers "openai" from "gpt-4o"
200
+ config: { model: 'anthropic/claude-3-opus' },
152
201
  },
153
- // provider not specified - router infers "openai"
202
+ provider: 'openrouter',
154
203
  mode: 'sync',
155
204
  };
156
-
157
- const res = await router.invoke(req);
205
+ await router.invoke(req);
158
206
  ```
159
207
 
160
- **Example 3: Using OpenRouter model format directly:**
208
+ **Provider inference from model name** (no `provider` field):
161
209
 
162
210
  ```ts
163
- // Call any OpenRouter-supported model using OpenRouter's format
164
211
  const req: AIRouterRequest = {
165
- request: {
166
- messages: [{ role: 'user', content: 'Hello!' }],
167
- config: { model: 'anthropic/claude-3-opus' }, // Direct OpenRouter format
168
- },
169
- provider: 'openrouter', // Use "openrouter" provider
212
+ request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'gpt-4o' } },
170
213
  mode: 'sync',
171
214
  };
172
-
173
- const res = await router.invoke(req);
215
+ await router.invoke(req); // infers openai
174
216
  ```
175
217
 
176
- **Example 4: Accessing models without provider packages:**
218
+ ### Troubleshooting
177
219
 
178
- ```ts
179
- // Access Meta Llama models without installing @x12i/ai-provider-meta
180
- const req: AIRouterRequest = {
181
- request: {
182
- messages: [{ role: 'user', content: 'Hello!' }],
183
- config: { model: 'meta-llama/llama-3-70b-instruct' },
184
- },
185
- provider: 'openrouter',
186
- mode: 'sync',
187
- };
220
+ If you see *"No provider specified and no providers registered"*:
188
221
 
189
- const res = await router.invoke(req);
190
- ```
222
+ 1. Confirm `OPENROUTER_API_KEY` (or `OPEN_ROUTER_KEY`) is set and non-empty
223
+ 2. Ensure the key does not start with `ENV.` (unresolved placeholder)
224
+ 3. Set `config.provider` in the request (e.g. `{ provider: 'openai', model: 'gpt-4o' }`)
225
+ 4. The OpenRouter adapter is always registered — no extra setup required
191
226
 
192
- **Example 5: Using diverse models from different providers:**
227
+ See also [debugging guide](./docs/debugging-no-provider-error.md).
193
228
 
194
- ```ts
195
- // Anthropic Claude models
196
- const claudeReq = { request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'claude-3-opus' } }, provider: 'anthropic', mode: 'sync' };
229
+ ---
230
+
231
+ ## Configuration
197
232
 
198
- // Google Gemini models
199
- const geminiReq = { request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'gemini-pro' } }, provider: 'google', mode: 'sync' };
233
+ ### Zero-config (`createRouter`)
200
234
 
201
- // Groq models (via xAI provider)
202
- const groqReq = { request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'llama-3-70b-8192' } }, provider: 'groq', mode: 'sync' };
235
+ ```ts
236
+ import { createRouter } from '@x12i/ai-providers-router';
203
237
 
204
- // All automatically route through OpenRouter when mode is enabled
205
- const results = await Promise.all([
206
- router.invoke(claudeReq),
207
- router.invoke(geminiReq),
208
- router.invoke(groqReq),
209
- ]);
238
+ const router = await createRouter(); // reads process.env
210
239
  ```
211
240
 
212
- ### How OpenRouter Mode Works
241
+ ### Programmatic (advanced mode)
213
242
 
214
- 1. **Request Interceptor**: When OpenRouter mode is enabled, a request interceptor:
215
- - Preserves the original provider name (e.g., `"openai"`, `"grok"`) in `request.config.provider`
216
- - Routes the request to `"openrouter"` provider
217
- - Infers provider from model name if not specified
243
+ ```ts
244
+ const router = await createRouter({
245
+ logLevel: 'info',
246
+ verbose: false,
247
+ timeoutMs: 60_000,
248
+ fallbackChain: [{ provider: 'openai', model: 'gpt-4o-mini' }, { provider: 'grok', model: 'grok-2' }],
249
+ openrouter: { apiKey: 'sk-or-...', httpReferer: 'https://example.com', xTitle: 'My App' },
250
+ usageTracker: {
251
+ recordRequest(e) {
252
+ // provider, timestamp, duration, tokens, cost, success
253
+ },
254
+ },
255
+ providerConfigs: {
256
+ openai: { apiKey: 'sk-...', baseURL: 'https://api.openai.com/v1' },
257
+ grok: { apiKey: 'xai-...' },
258
+ },
259
+ });
260
+ ```
218
261
 
219
- 2. **Model Name Mapping**: The `OpenRouterAdapter`:
220
- - Reads the original provider from `request.config.provider`
221
- - Maps model names: `"gpt-4o"` + `provider: "openai"` → `"openai/gpt-4o"`
222
- - Handles models already in OpenRouter format (with `/`) as-is
262
+ Passing any explicit config object to `createRouter(config)` overrides zero-config env discovery for that call.
263
+
264
+ ### Environment variables
265
+
266
+ | Variable | Default | Description |
267
+ |----------|---------|-------------|
268
+ | **Router** | | |
269
+ | `AI_PROVIDER_ROUTER_LOGS_LEVEL` | *(see logging)* | Canonical log threshold via logxer (`error`, `warn`, `info`, `debug`, `verbose`, `off`) |
270
+ | `AI_PROVIDER_ROUTER_LOG_LEVEL` | `info` | Legacy alias for log level (used when `_LOGS_LEVEL` is unset) |
271
+ | `AI_PROVIDER_ROUTER_VERBOSE` | `false` | Log full AI request/response payloads (sanitized) |
272
+ | `AI_PROVIDER_ROUTER_TIMEOUT_MS` | `60000` | Default operation timeout (ms) |
273
+ | **OpenAI** | | |
274
+ | `OPENAI_API_KEY` | — | Required for direct OpenAI calls |
275
+ | `OPENAI_API_BASE` | — | Custom API base URL |
276
+ | `OPENAI_ORGANIZATION` | — | Organization ID |
277
+ | **Grok / xAI** | | |
278
+ | `GROK_API_KEY` | — | Required for direct Grok calls |
279
+ | `XAI_API_BASE` | — | Custom xAI base URL |
280
+ | **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 |
284
+ | `OPENROUTER_HTTP_REFERER` | — | Optional ranking header |
285
+ | `OPENROUTER_X_TITLE` | — | Optional ranking header |
286
+ | **Other providers** | | |
287
+ | `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, `GROQ_API_KEY`, … | — | Used when those providers are installed |
288
+
289
+ Full reference: [Environment variables](./docs/environment-variables.md) · [Configuration guide](./docs/CONFIGURATION_GUIDE.md)
223
290
 
224
- 3. **Response Parsing**: Responses are parsed directly from OpenAI formats (no ai-io-normalizer):
225
- - **Chat Completions**: Extracts `choices[0].message.content` for text
226
- - **Responses API (v1)**: Handles `output` array with text and encrypted reasoning items
227
- - Extracts `usage` for token counts from both formats
228
- - Adds `status: 'completed'` for compatibility
291
+ ---
292
+
293
+ ## Logging
229
294
 
230
- ### Provider Inference Rules
295
+ The router uses [`@x12i/logxer`](https://www.npmjs.com/package/@x12i/logxer) for structured, package-scoped logging.
231
296
 
232
- When no provider is specified, the router uses **catalog data** to intelligently infer providers from model names. This includes:
297
+ **Package prefix:** `AI_PROVIDER_ROUTER`
233
298
 
234
- - **Exact Model Matching**: Recognizes all 353 OpenRouter models by their exact IDs
235
- - **Alias Support**: Handles model aliases from the catalog
236
- - **Vendor Mapping**: Maps vendor IDs to provider slugs (e.g., `xai` → `grok`)
237
- - **Fallback Patterns**: Uses legacy pattern matching when catalog data is unavailable:
299
+ ```bash
300
+ # Canonical (preferred)
301
+ AI_PROVIDER_ROUTER_LOGS_LEVEL=info
238
302
 
239
- - `gpt-*`, `o1-*`, `openai/*` `"openai"`
240
- - `claude-*`, `anthropic/*` → `"anthropic"`
241
- - `grok-*`, `xai/*` → `"grok"`
242
- - `gemini-*`, `google/*` → `"google"`
243
- - `llama-*`, `meta-llama/*` → `"meta"`
244
- - Default → `"openai"` (most common case)
303
+ # Legacy (still supported when _LOGS_LEVEL is unset)
304
+ AI_PROVIDER_ROUTER_LOG_LEVEL=info
245
305
 
246
- ### Model Validation & Catalog Features
306
+ # Log full AI request/response payloads (router-specific, separate from log level)
307
+ AI_PROVIDER_ROUTER_VERBOSE=true
308
+ ```
247
309
 
248
- The router automatically validates models against the OpenRouter catalog:
310
+ **Log levels:** `error` · `warn` · `info` · `debug` · `verbose` · `off`
249
311
 
250
- - **Model Availability**: Warns when requesting models not available in OpenRouter
251
- - **Alias Resolution**: Automatically resolves model aliases to canonical OpenRouter IDs
252
- - **Capability Checking**: Validates model parameters against supported capabilities
253
- - **Graceful Fallbacks**: Falls back to legacy logic if catalog loading fails
254
- - **Format Support**: Handles both OpenAI Chat Completions and Responses API v1 formats
255
- - **Encrypted Reasoning**: Processes encrypted reasoning traces (model thinking is privacy-protected)
256
- - **Reasoning Parameter Support**: Enables reasoning effort levels for compatible models
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`.
257
313
 
258
- **Catalog Data Sources:**
259
- - **67 Providers**: All current OpenRouter providers
260
- - **353 Models**: Complete model catalog with aliases and capabilities
261
- - **Vendor Mappings**: Direct API mappings for accurate routing
262
- - **Auto-updating**: Uses latest catalog data from OpenRouter APIs
314
+ **Programmatic:**
263
315
 
264
- ### OpenRouter Configuration
316
+ ```ts
317
+ import { createRouter, createLogger, getLogger } from '@x12i/ai-providers-router';
265
318
 
266
- Optional environment variables for OpenRouter rankings:
319
+ const router = await createRouter({ logLevel: 'debug', verbose: true });
267
320
 
268
- ```bash
269
- export OPEN_ROUTER_HTTP_REFERER=https://your-site.com
270
- export OPEN_ROUTER_X_TITLE=Your Site Name
321
+ // Or inject a custom logger instance
322
+ const router2 = await createRouter({ logger: createLogger({ level: 'info', verbose: false }) });
271
323
  ```
272
324
 
273
- See [Environment Variables documentation](./docs/environment-variables.md) for details.
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.
274
326
 
275
327
  ---
276
328
 
277
- ## Zero-config router creation
329
+ ## API usage
278
330
 
279
- No arguments are required.
331
+ ### Sync call
280
332
 
281
333
  ```ts
282
- import { createRouter } from '@x12i/ai-providers-router';
334
+ import { createRouter, type AIRouterRequest, type AIResponse } from '@x12i/ai-providers-router';
283
335
 
284
336
  const router = await createRouter();
337
+
338
+ const req: AIRouterRequest = {
339
+ request: {
340
+ inputData: 'Write 3 bullets about routers.',
341
+ config: { model: 'gpt-4o-mini', maxTokens: 200, temperature: 0.7 },
342
+ },
343
+ provider: 'openai',
344
+ mode: 'sync',
345
+ exec: {
346
+ timeoutMs: 60_000,
347
+ idempotencyKey: 'optional-key',
348
+ signal: abortController.signal,
349
+ },
350
+ };
351
+
352
+ const res: AIResponse = await router.invoke(req);
285
353
  ```
286
354
 
287
- Optional router-level config (logging, usage tracking, timeout):
355
+ ### Streaming call
288
356
 
289
357
  ```ts
290
- const router = await createRouter({
291
- logLevel: 'info',
292
- verbose: false,
293
- timeoutMs: 60000, // Default timeout for all operations (ERC: AI_PROVIDER_ROUTER_TIMEOUT_MS)
294
- usageTracker: {
295
- recordRequest(e) { /* ... */ },
296
- },
358
+ const streamReq: AIRouterRequest = { ...req, mode: 'stream' };
359
+
360
+ for await (const ev of router.stream(streamReq)) {
361
+ switch (ev.type) {
362
+ case 'provider_raw':
363
+ console.log('Raw:', ev.raw);
364
+ break;
365
+ case 'output_text_delta':
366
+ process.stdout.write(ev.delta);
367
+ break;
368
+ case 'reasoning_summary_delta':
369
+ case 'reasoning_trace_delta':
370
+ // reasoning stream chunks
371
+ break;
372
+ case 'completed':
373
+ console.log('Final:', ev.response.outputText);
374
+ break;
375
+ case 'error':
376
+ console.error(ev.error);
377
+ break;
378
+ }
379
+ }
380
+ ```
381
+
382
+ ### Batch requests
383
+
384
+ Batch is available only when `provider.capabilities.modes.batch === true`:
385
+
386
+ ```ts
387
+ const items = [
388
+ { request: { inputData: 'First', config: { model: 'gpt-4o-mini' } } },
389
+ { request: { inputData: 'Second', config: { model: 'gpt-4o-mini' } } },
390
+ ];
391
+
392
+ const batchResult = await router.createBatch('openai', items, {
393
+ timeoutMs: 120_000,
394
+ idempotencyKey: 'batch-1',
297
395
  });
396
+
397
+ console.log(batchResult.items);
398
+ console.log(batchResult.rawBatch);
298
399
  ```
299
400
 
300
- ---
401
+ ### Request and response types
301
402
 
302
- ## Request/Response Types
403
+ | Type | Purpose |
404
+ |------|---------|
405
+ | `AIRouterRequest` | Router input (`request`, `provider`, `mode`, `exec`) |
406
+ | `AIResponse` | Sync output (`outputText`, `rawResponse`, `usage`, `reasoning`, `metadata`) |
407
+ | `AIStreamEvent` | Streaming events (`output_text_delta`, `completed`, `error`, …) |
408
+ | `AIBatchResponse` | Batch results |
409
+ | `RouterConfig` | Router-level settings |
410
+ | `ProviderModelRef` | `{ provider?, engine?, model? }` for fallback chains |
303
411
 
304
- Router uses its own request/response types:
412
+ ### Trace diagnostics
305
413
 
306
- * `AIRouterRequest` (input) - includes unified reasoning controls
307
- * `AIResponse` (sync output) - includes unified reasoning response
308
- * `AIStreamEvent` (streaming output) - includes reasoning streaming events
309
- * `AIBatchResponse` (batch output)
414
+ Every `AIResponse` includes stable, provider-agnostic diagnostics in `metadata`:
310
415
 
311
- ### Authoritative trace diagnostics (stable contract)
416
+ | Field | Description |
417
+ |-------|-------------|
418
+ | `metadata.provider` | Final provider used |
419
+ | `metadata.modelUsed` | Actual model that served the response |
420
+ | `metadata.costUsd` / `metadata.cost` | USD cost when reported (e.g. OpenRouter `usage.cost`) |
421
+ | `metadata.costStatus` | `'priced'` or `'unpriced'` |
422
+ | `metadata.maxTokensRequested` | Effective generation cap |
423
+ | `metadata.requestIds` | `{ routerRequestId, providerRequestId?, openrouterRequestId? }` |
424
+ | `metadata.timing` | `{ startedAt, endedAt, durationMs }` |
425
+ | `metadata.latencyMs` | Alias for `timing.durationMs` |
426
+ | `metadata.attempts[]` | Ordered retry + fallback trace |
427
+ | `response.output.parsed` | Structured fields when `outputContract` is set |
312
428
 
313
- For downstream orchestration, `AIResponse` includes stable, provider-agnostic diagnostics:
429
+ ### Reasoning
314
430
 
315
- - `response.usage?: { promptTokens; completionTokens; totalTokens }`
316
- - `response.metadata` (keys when known):
317
- - `metadata.provider`: final provider used for the successful call (or last attempt)
318
- - `metadata.modelUsed`: the actual model that served the response
319
- - `metadata.maxTokensRequested`: final effective generation cap applied (if determinable)
320
- - `metadata.costUsd` / `metadata.cost`: normalized USD cost when the provider reports it (e.g. OpenRouter `usage.cost`)
321
- - `metadata.costStatus`: `'priced'` when `costUsd` is set; `'unpriced'` when usage exists but no cost was returned
322
- - `response.output.parsed`: structured fields when `outputContract` is on the request (markdown sections → camelCase keys)
323
- - `metadata.requestIds`: `{ routerRequestId, providerRequestId?, openrouterRequestId? }`
324
- - `metadata.timing`: `{ startedAt, endedAt, durationMs }` (provider-call timing)
325
- - `metadata.latencyMs`: alias for `metadata.timing.durationMs`
326
- - `metadata.attempts[]`: ordered attempts across retries + fallbacks (authoritative execution trace)
431
+ Request unified reasoning controls via `request.config.reasoning`:
327
432
 
328
433
  ```ts
329
- import type { AIRouterRequest, AIResponse } from '@x12i/ai-providers-router';
330
-
331
- // Request reasoning with extended effort levels
332
434
  config: {
333
435
  reasoning: {
334
- effort: 'high', // or 'low', 'medium', 'high', 'xhigh' (xhigh normalized to high)
335
- maxTokens: 2000, // optional: for Anthropic/Gemini models (max_tokens mode)
336
- visibility: 'trace', // or 'none', 'summary' (best-effort; downgraded if not returned)
337
- onUnsupported: 'downgrade' // or 'error' (throws), 'ignore' (silent)
338
- }
436
+ effort: 'high', // low | medium | high | xhigh (xhigh high)
437
+ maxTokens: 2000, // Anthropic/Gemini max_tokens mode
438
+ visibility: 'trace', // none | summary | trace (best-effort)
439
+ onUnsupported: 'downgrade', // downgrade | error | ignore
440
+ },
339
441
  }
340
-
341
- // Access unified reasoning response
342
- response.reasoning.artifacts.encrypted // Encrypted reasoning traces
343
- response.reasoning.applied.effort // What was actually applied (may differ from requested)
344
- response.reasoning.applied.visibility // What visibility was actually returned
345
- response.reasoning.availability // Model capability flags
346
- response.reasoning.warnings // Any downgrade/normalization warnings
347
442
  ```
348
443
 
349
- **Reasoning Features:**
350
- - ✅ **Effort Control**: `low`, `medium`, `high`, `xhigh` (xhigh auto-normalized to high)
351
- - ✅ **Max Tokens Control**: Direct `maxTokens` budget for Anthropic/Gemini models
352
- - ✅ **Encrypted Traces**: Access encrypted reasoning artifacts (ciphertext not decryptable by user; only metadata/prefix logged)
353
- - ✅ **Summary Visibility**: Human-readable reasoning summary (best-effort; returned only if provider returns `reasoning_details` with `reasoning.summary`; otherwise downgraded with warning)
354
- - ✅ **Trace Visibility**: Encrypted or readable reasoning traces (best-effort; satisfied by either `reasoning.encrypted` artifacts or `reasoning.text` chunks; downgraded if not available)
355
- - ✅ **Model Detection**: Automatic detection of reasoning-capable models via JSON registry (cross-vendor support)
356
- - ✅ **Extended Support**: Works with OpenAI o-series models (o1, o3, o4 series - 10+ models), xAI Grok models, Anthropic Claude reasoning models, and Google Gemini reasoning models
444
+ Response fields: `response.reasoning.applied`, `response.reasoning.artifacts`, `response.reasoning.warnings`.
357
445
 
358
- **Supported Models**: Currently detected via router-owned JSON registry (`.metadata/reasoning-support.json`):
359
- - **OpenAI o-series** (`openai/o*` pattern): `openai/o1`, `openai/o1-pro`, `openai/o3`, `openai/o3-mini`, `openai/o3-pro`, `openai/o3-deep-research`, `openai/o3-mini-high`, `openai/o4-mini`, `openai/o4-mini-deep-research`, `openai/o4-mini-high`
360
- - **xAI Grok** (`x-ai/grok*` pattern): `x-ai/grok-4.1-fast` and other reasoning-enabled Grok models
361
- - **Anthropic Claude** (`anthropic/claude*` pattern): Reasoning-enabled Claude models (uses `max_tokens` mode)
362
- - **Google Gemini** (`google/gemini*` pattern): Reasoning-enabled Gemini models (uses `max_tokens` mode)
446
+ Supported models are tracked in `.metadata/reasoning-support.json`.
363
447
 
364
- > ℹ️ **Note**: Summary/trace visibility are **best-effort** and depend on what the provider actually returns in `reasoning_details`. If the provider doesn't return the requested visibility type, the router downgrades to `none` and adds a `VISIBILITY_DOWNGRADED` warning. Encrypted reasoning artifacts are **not decryptable** by the user; only metadata (id, format, index) and a ciphertext prefix (first 32 chars) are logged for debugging. Many other vendors have reasoning-capable models (Amazon Nova, Aion Labs, Alibaba Tongyi, AllenAI OLMO, Arcee AI, Baidu ERNIE, ByteDance Seed, DeepCogito, MoonshotAI Kimi, Qwen, THUDM GLM, and more), including models with "thinking" or "thought" capabilities, but they are not yet implemented. See [Reasoning Supported Models](./docs/reasoning-supported-models.md) for the complete list.
448
+ - [Reasoning integration guide](./docs/reasoning-integration.md)
449
+ - [Supported models](./docs/reasoning-supported-models.md)
365
450
 
366
- See [Reasoning Integration Guide](./docs/reasoning-integration.md) and [Reasoning Supported Models](./docs/reasoning-supported-models.md) for complete documentation.
451
+ ### Fallback chains
367
452
 
368
- ---
453
+ On failure, the router tries the next candidate in order. Attempts are recorded in `metadata.attempts[]`. On exhaustion, throws `FallbackExhaustedError`.
369
454
 
370
- ## Sync call
455
+ **Router-level default chain:**
371
456
 
372
457
  ```ts
373
- import { createRouter, type AIRouterRequest, type AIResponse } from '@x12i/ai-providers-router';
458
+ const router = await createRouter({
459
+ fallbackChain: [
460
+ { provider: 'openai', model: 'gpt-4o' },
461
+ { provider: 'grok', model: 'grok-2' },
462
+ ],
463
+ });
464
+ ```
374
465
 
375
- const router = await createRouter();
466
+ **Per-request chain** (in `request.config`):
376
467
 
377
- const req: AIRouterRequest = {
378
- request: {
379
- inputData: 'Write 3 bullets about routers.',
380
- config: {
381
- maxTokens: 200,
382
- temperature: 0.7,
383
- model: 'gpt-4o-mini',
384
- },
385
- },
386
- provider: 'openai',
387
- mode: 'sync',
388
- exec: {
389
- timeoutMs: 60000, // Optional: override default timeout
390
- idempotencyKey: 'optional-key', // Optional: for idempotent requests
468
+ ```ts
469
+ request: {
470
+ config: {
471
+ model: 'gpt-4o',
472
+ fallbackChain: [
473
+ { provider: 'openai', model: 'gpt-4o-mini' },
474
+ { engine: 'grok', model: 'grok-2' }, // engine is alias for provider
475
+ ],
476
+ // Legacy: provider-only fallback (same model)
477
+ // fallbackProviders: ['grok', 'openai'],
391
478
  },
392
- };
479
+ },
480
+ ```
393
481
 
394
- const res: AIResponse = await router.invoke(req);
482
+ Precedence: `request.config.fallbackChain` `request.config.fallbackEngines` → `router.fallbackChain` → `request.config.fallbackProviders`.
395
483
 
396
- console.log(res.outputText); // Normalized text (optional)
397
- console.log(res.rawResponse); // Lossless raw response (always present)
398
- console.log(res.usage); // Token usage
484
+ ### Interceptors
485
+
486
+ ```ts
487
+ router.addRequestInterceptor(async (req, provider) => {
488
+ // mutate or replace request before execution
489
+ return req;
490
+ });
491
+
492
+ router.addResponseInterceptor(async (res, provider) => {
493
+ // mutate or replace response after execution
494
+ return res;
495
+ });
399
496
  ```
400
497
 
498
+ OpenRouter mode registers a request interceptor automatically to route vendor calls through OpenRouter while preserving the original provider name for model mapping.
499
+
500
+ ### Health checks
501
+
502
+ ```ts
503
+ const result = await router.checkHealth('openai');
504
+ // { provider: 'openai', healthy: true, latencyMs: 1234 }
505
+ // or { provider: 'openai', healthy: false, latencyMs: 5000, error: '...' }
506
+ ```
507
+
508
+ Runs a minimal sync invoke with a 5 s timeout.
509
+
401
510
  ---
402
511
 
403
- ## Streaming call
512
+ ## AIGateway
513
+
514
+ Thin wrapper around the router for gateway-style requests (instructions + inputData):
404
515
 
405
516
  ```ts
406
- const streamReq: AIRouterRequest = {
407
- ...req,
408
- mode: 'stream',
409
- };
517
+ import { AIGateway, createRouter } from '@x12i/ai-providers-router';
410
518
 
411
- for await (const ev of router.stream(streamReq)) {
412
- if (ev.type === 'provider_raw') {
413
- // Raw provider event (always emitted for debugging)
414
- console.log('Raw event:', ev.raw);
415
- } else if (ev.type === 'output_text_delta') {
416
- // Normalized text delta
417
- process.stdout.write(ev.delta);
418
- } else if (ev.type === 'completed') {
419
- // Final response
420
- console.log('Final:', ev.response.outputText);
421
- } else if (ev.type === 'error') {
422
- console.error('Error:', ev.error);
423
- }
424
- }
519
+ const gateway = new AIGateway(await createRouter());
520
+
521
+ const response = await gateway.invoke({
522
+ instructions: 'You are a helpful assistant.',
523
+ inputData: 'Explain routers in one sentence.',
524
+ config: { provider: 'openai', model: 'gpt-4o-mini' },
525
+ mode: 'sync',
526
+ });
425
527
  ```
426
528
 
529
+ Also accepts full `AIRouterRequest` shapes (`{ request, provider, mode }`) and unwraps them automatically.
530
+
531
+ Optional strict provider/model pinning: set `config.enforceProviderModel: true` to throw on mismatch instead of silently switching.
532
+
427
533
  ---
428
534
 
429
- ## Batch requests
535
+ ## Response normalization and cost
430
536
 
431
- Batch requests use the batch API (gated by ProviderModule capabilities):
537
+ Exported helpers for downstream activity persistence and output contracts:
432
538
 
433
539
  ```ts
434
- const items = [
435
- { request: { inputData: 'First request', config: { model: 'gpt-4o-mini' } } },
436
- { request: { inputData: 'Second request', config: { model: 'gpt-4o-mini' } } },
437
- ];
540
+ import {
541
+ applyResponseNormalization,
542
+ resolveCostReporting,
543
+ extractCostUsdFromRouterResponse,
544
+ extractCostUsdFromProviderUsage,
545
+ enrichParsedForOutputContract,
546
+ resolveOutputContractFieldKeys,
547
+ parseMarkdownSectionsFromContent,
548
+ } from '@x12i/ai-providers-router';
549
+ ```
438
550
 
439
- const batchResult = await router.createBatch('openai', items, {
440
- timeoutMs: 120000, // Optional: override default timeout
441
- idempotencyKey: 'optional-key', // Optional
442
- });
551
+ - **Cost** Normalizes OpenRouter and provider usage into `metadata.costUsd` / `costStatus`
552
+ - **Output contract** — When `outputContract` is on the request, markdown sections map to camelCase keys in `output.parsed`
553
+
554
+ See [normalization field support](./docs/normalization-field-support.md).
555
+
556
+ ---
557
+
558
+ ## Error types
559
+
560
+ | Error | When |
561
+ |-------|------|
562
+ | `ProviderNotFoundError` | Requested provider is not registered |
563
+ | `ProviderNotInstalledError` | Provider package not installed (includes `npm install` hint) |
564
+ | `ProviderTimeoutError` | Request exceeded `timeoutMs` (`code: 'ETIMEDOUT'`) |
565
+ | `FallbackExhaustedError` | All fallback candidates failed; check `.attempts[]` |
443
566
 
444
- console.log(batchResult.items); // Array of results
445
- console.log(batchResult.rawBatch); // Lossless raw batch response
567
+ On partial provider failures, `FallbackExhaustedError` may carry a router-shaped partial payload for gateway extraction (`PartialRouterPayload`).
568
+
569
+ ---
570
+
571
+ ## Manual setup (advanced)
572
+
573
+ For full control without `createRouter()`:
574
+
575
+ ```ts
576
+ import { LLMProviderRouter } from '@x12i/ai-providers-router';
577
+ import * as openaiModule from '@x12i/ai-provider-openai';
578
+
579
+ const router = new LLMProviderRouter({ logLevel: 'info', timeoutMs: 60_000 });
580
+
581
+ router.configureProvider('openai', { apiKey: process.env.OPENAI_API_KEY! });
582
+ router.registerProvider(openaiModule, 'initializeClient');
583
+
584
+ const providers = router.listProviders(); // ['openai']
585
+ const registry = router.getProviderRegistry();
586
+ const adapters = router.getAdapterRegistry();
446
587
  ```
447
588
 
448
- **Note**: Batch is only available if `provider.capabilities.modes.batch === true`. Router gates execution by ProviderModule capabilities, not transformer supports.
589
+ Providers are also **auto-registered on first invoke** when matching API keys are in the environment (unless OpenRouter mode is active).
590
+
591
+ Legacy config file support:
592
+
593
+ ```ts
594
+ import { createRouterFromConfig } from '@x12i/ai-providers-router';
595
+ const router = await createRouterFromConfig('./router-config.json');
596
+ ```
449
597
 
450
598
  ---
451
599
 
452
- ## How it works (high level)
453
-
454
- 1. Router receives an `AIRouterRequest`
455
- 2. **Request Interceptors** (if OpenRouter mode enabled):
456
- - Preserve original provider name for model mapping
457
- - Route requests to OpenRouter provider
458
- - Infer provider from model name if not specified
459
- 3. Router loads ProviderModule from installed provider package (lazy import)
460
- 4. Router checks `provider.capabilities.modes` to gate execution
461
- 5. Router-side adapter converts request to `ProviderSDKCallSpec`
462
- - **OpenRouterAdapter**: Maps provider + model to OpenRouter format (e.g., `"openai/gpt-4o"`)
463
- 6. Router calls ProviderModule:
464
-
465
- * `provider.execute(spec)` (sync)
466
- * `provider.stream(spec)` (streaming)
467
- * `provider.submitBatch(specs)` (batch)
468
- 7. Router-side adapter parses `ProviderSDKExecResult` to `AIResponse`
469
- - **OpenRouterAdapter**: Parses OpenAI Chat Completions format directly (no ai-io-normalizer)
470
- 8. Router returns standardized response with lossless `rawResponse`
600
+ ## Public API exports
601
+
602
+ ```ts
603
+ // Router
604
+ export { LLMProviderRouter, createRouter, createRouterFromConfig }
605
+
606
+ // Types
607
+ export type { RouterConfig, AIRouterRequest, AIResponse, AIStreamEvent,
608
+ AIBatchResponse, AIBatchRequestItem, NormalizedRouterOutput, ProviderModelRef,
609
+ HealthCheckResult, ProviderId, CreateRouterConfig }
610
+
611
+ // Errors
612
+ export { ProviderNotFoundError, FallbackExhaustedError,
613
+ ProviderNotInstalledError, ProviderTimeoutError }
614
+ export type { FallbackAttempt, PartialRouterPayload }
615
+
616
+ // Interceptors
617
+ export type { RequestInterceptor, ResponseInterceptor }
618
+
619
+ // Logger
620
+ export { Logger, getLogger, createLogger }
621
+ export type { LogLevel, LoggerConfig }
622
+
623
+ // Gateway
624
+ export { AIGateway }
625
+ export type { EnhancedLLMResponse }
626
+
627
+ // Normalization
628
+ export { applyResponseNormalization, resolveCostReporting,
629
+ extractCostUsdFromRouterResponse, extractCostUsdFromProviderUsage,
630
+ hasNonZeroTokenUsage, enrichParsedForOutputContract,
631
+ resolveOutputContractFieldKeys, contractSpecToFieldKeys,
632
+ parseMarkdownSectionsFromContent }
633
+ export type { ActivityCostStatus, ResolvedCostReporting }
634
+
635
+ // Registries and adapters (advanced)
636
+ export { ProviderRegistry, AdapterRegistry, OpenAIAdapter, GrokAdapter }
637
+ ```
471
638
 
472
639
  ---
473
640
 
474
- ## Provider packages are required
641
+ ## Provider packages
642
+
643
+ | Provider ID | Package | API key env |
644
+ |-------------|---------|-------------|
645
+ | `openai` | `@x12i/ai-provider-openai` | `OPENAI_API_KEY` |
646
+ | `grok` | `@x12i/ai-provider-grok` | `GROK_API_KEY` |
647
+ | `anthropic` | `@x12i/ai-provider-anthropic` | `ANTHROPIC_API_KEY` |
648
+ | `google` | `@x12i/ai-provider-google` | `GOOGLE_API_KEY` |
649
+ | `groq` | `@x12i/ai-provider-groq` | `GROQ_API_KEY` |
650
+ | OpenRouter mode | `@x12i/ai-provider-openai` (bundled) | `OPENROUTER_API_KEY` |
475
651
 
476
- If you call a provider that is not installed, the router throws a clear error with install instructions.
652
+ Missing packages produce a clear `ProviderNotInstalledError` with install instructions.
477
653
 
478
- **Exception**: When OpenRouter mode is enabled, you only need `@x12i/ai-provider-openai` installed (OpenRouter uses OpenAI-compatible API). You can access **any of the 353 models from 67 providers** without installing individual provider packages.
654
+ ---
479
655
 
480
- **Supported Providers in OpenRouter Mode:**
481
- - All major providers: OpenAI, Anthropic, Google, xAI (Grok), Groq, Meta, Mistral, Cohere, etc.
482
- - 67 total providers from the OpenRouter catalog
483
- - 353 models with full capability support
656
+ ## Development and testing
484
657
 
485
- Examples:
658
+ ```bash
659
+ npm run build # compile TypeScript
660
+ npm test # build + run all .tests/**/*.test.js
661
+ npm run test:openai # live OpenAI call (requires OPENAI_API_KEY)
662
+ npm run test:openrouter
663
+ npm run test:reasoning
664
+ npm run erc:verify # ERC manifest verification
665
+ ```
666
+
667
+ Requires **Node.js ≥ 18**.
668
+
669
+ ---
486
670
 
487
- * Provider `openai` requires `@x12i/ai-provider-openai`
488
- * Provider `grok` requires `@x12i/ai-provider-grok`
489
- * **OpenRouter mode**: Only requires `@x12i/ai-provider-openai` to access all OpenRouter-supported models
671
+ ## Related documentation
490
672
 
491
- This router will never auto-install packages.
673
+ | Document | Topic |
674
+ |----------|-------|
675
+ | [Configuration guide](./docs/CONFIGURATION_GUIDE.md) | Full request/config reference |
676
+ | [Environment variables](./docs/environment-variables.md) | Complete env var list |
677
+ | [Reasoning integration](./docs/reasoning-integration.md) | Reasoning API details |
678
+ | [Reasoning supported models](./docs/reasoning-supported-models.md) | Model registry |
679
+ | [Request/response flow](./docs/request-response-flow.md) | Internal flow |
680
+ | [Debugging no-provider error](./docs/debugging-no-provider-error.md) | OpenRouter troubleshooting |
681
+ | [Normalization fields](./docs/normalization-field-support.md) | Output contract and cost |
492
682
 
493
683
  ---
494
684
 
495
685
  ## License
496
686
 
497
- ISC
687
+ MIT
package/dist/logger.d.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * Logging utility for AI Provider Router
3
3
  * Provides structured logging with proper log levels and verbose mode support
4
4
  */
5
- export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose';
5
+ import { type LogLevel } from '@x12i/logxer';
6
+ export type { LogLevel };
6
7
  export interface LoggerConfig {
7
8
  verbose?: boolean;
8
9
  level?: LogLevel;
@@ -19,46 +20,42 @@ export declare class Logger {
19
20
  * Update logger configuration
20
21
  */
21
22
  setConfig(config: LoggerConfig): void;
22
- /**
23
- * Check if a log level should be logged
24
- */
25
- private shouldLog;
26
23
  /**
27
24
  * Log error messages
28
25
  */
29
- error(message: string, data?: any): void;
26
+ error(message: string, data?: Record<string, unknown>): void;
30
27
  /**
31
28
  * Log warning messages
32
29
  */
33
- warn(message: string, data?: any): void;
30
+ warn(message: string, data?: Record<string, unknown>): void;
34
31
  /**
35
32
  * Log informational messages
36
33
  */
37
- info(message: string, data?: any): void;
34
+ info(message: string, data?: Record<string, unknown>): void;
38
35
  /**
39
36
  * Log debug messages
40
37
  */
41
- debug(message: string, data?: any): void;
38
+ debug(message: string, data?: Record<string, unknown>): void;
42
39
  /**
43
40
  * Log verbose messages (only when verbose mode is enabled)
44
41
  */
45
- logVerbose(message: string, data?: any): void;
42
+ logVerbose(message: string, data?: Record<string, unknown>): void;
46
43
  /**
47
44
  * Log AI request (unfiltered, only in verbose mode)
48
45
  */
49
- logAIRequest(provider: string, request: any, metadata?: any): void;
46
+ logAIRequest(provider: string, request: unknown, metadata?: Record<string, unknown>): void;
50
47
  /**
51
48
  * Log AI response (unfiltered, only in verbose mode)
52
49
  */
53
- logAIResponse(provider: string, response: any, metadata?: any): void;
50
+ logAIResponse(provider: string, response: unknown, metadata?: Record<string, unknown>): void;
54
51
  /**
55
52
  * Log AI request/response pair (unfiltered, only in verbose mode)
56
53
  */
57
- logAIIteraction(provider: string, request: any, response: any, duration?: number, metadata?: any): void;
54
+ logAIIteraction(provider: string, request: unknown, response: unknown, duration?: number, metadata?: Record<string, unknown>): void;
58
55
  /**
59
56
  * Sanitize data for logging (remove sensitive info, handle circular refs)
60
57
  */
61
- sanitizeForLogging(data: any): any;
58
+ sanitizeForLogging(data: unknown): unknown;
62
59
  }
63
60
  /**
64
61
  * Get or create the default logger instance
package/dist/logger.js CHANGED
@@ -2,16 +2,23 @@
2
2
  * Logging utility for AI Provider Router
3
3
  * Provides structured logging with proper log levels and verbose mode support
4
4
  */
5
- import logxer from '@x12i/logxer';
6
- const logs = logxer || console;
5
+ import { createLogxer } from '@x12i/logxer';
6
+ const LOGXER_PACKAGE = {
7
+ packageName: 'AI Provider Router',
8
+ envPrefix: 'AI_PROVIDER_ROUTER',
9
+ debugNamespace: 'ai-providers-router',
10
+ };
11
+ function createGateway(level) {
12
+ return createLogxer(LOGXER_PACKAGE, level !== undefined ? { logLevel: level } : undefined);
13
+ }
7
14
  /**
8
15
  * Logger class that wraps logxer with proper log levels
9
16
  */
10
17
  export class Logger {
11
18
  constructor(config = {}) {
12
- this.verbose = config.verbose || false;
13
- this.level = config.level || 'info';
14
- this.gateway = logs || console;
19
+ this.verbose = config.verbose ?? false;
20
+ this.gateway = createGateway(config.level);
21
+ this.level = config.level ?? this.gateway.getConfig().logLevel;
15
22
  }
16
23
  /**
17
24
  * Update logger configuration
@@ -22,84 +29,40 @@ export class Logger {
22
29
  }
23
30
  if (config.level !== undefined) {
24
31
  this.level = config.level;
32
+ this.gateway = createGateway(config.level);
25
33
  }
26
34
  }
27
- /**
28
- * Check if a log level should be logged
29
- */
30
- shouldLog(level) {
31
- const levels = ['error', 'warn', 'info', 'debug', 'verbose'];
32
- const currentIndex = levels.indexOf(this.level);
33
- const messageIndex = levels.indexOf(level);
34
- return messageIndex <= currentIndex;
35
- }
36
35
  /**
37
36
  * Log error messages
38
37
  */
39
38
  error(message, data) {
40
- if (!this.shouldLog('error'))
41
- return;
42
- if (this.gateway?.error) {
43
- this.gateway.error(message, data || {});
44
- }
45
- else {
46
- console.error(`[ERROR] ${message}`, data || '');
47
- }
39
+ this.gateway.error(message, data ?? {});
48
40
  }
49
41
  /**
50
42
  * Log warning messages
51
43
  */
52
44
  warn(message, data) {
53
- if (!this.shouldLog('warn'))
54
- return;
55
- if (this.gateway?.warn) {
56
- this.gateway.warn(message, data || {});
57
- }
58
- else {
59
- console.warn(`[WARN] ${message}`, data || '');
60
- }
45
+ this.gateway.warn(message, data ?? {});
61
46
  }
62
47
  /**
63
48
  * Log informational messages
64
49
  */
65
50
  info(message, data) {
66
- if (!this.shouldLog('info'))
67
- return;
68
- if (this.gateway?.info) {
69
- this.gateway.info(message, data || {});
70
- }
71
- else {
72
- console.log(`[INFO] ${message}`, data || '');
73
- }
51
+ this.gateway.info(message, data ?? {});
74
52
  }
75
53
  /**
76
54
  * Log debug messages
77
55
  */
78
56
  debug(message, data) {
79
- if (!this.shouldLog('debug'))
80
- return;
81
- if (this.gateway?.debug) {
82
- this.gateway.debug(message, data || {});
83
- }
84
- else {
85
- console.debug(`[DEBUG] ${message}`, data || '');
86
- }
57
+ this.gateway.debug(message, data ?? {});
87
58
  }
88
59
  /**
89
60
  * Log verbose messages (only when verbose mode is enabled)
90
61
  */
91
62
  logVerbose(message, data) {
92
- if (!this.verbose || !this.shouldLog('verbose'))
63
+ if (!this.verbose)
93
64
  return;
94
- if (this.gateway?.verbose) {
95
- this.gateway.verbose(message, data || {});
96
- }
97
- else if (this.gateway?.trace) {
98
- this.gateway.trace(message, data || {});
99
- }
100
- else {
101
- console.log(`[VERBOSE] ${message}`, data || '');
102
- }
65
+ this.gateway.verbose(message, data ?? {});
103
66
  }
104
67
  /**
105
68
  * Log AI request (unfiltered, only in verbose mode)
@@ -110,7 +73,7 @@ export class Logger {
110
73
  this.logVerbose('AI Request Sent', {
111
74
  provider,
112
75
  request: this.sanitizeForLogging(request),
113
- metadata: metadata || {},
76
+ metadata: metadata ?? {},
114
77
  timestamp: new Date().toISOString(),
115
78
  });
116
79
  }
@@ -123,7 +86,7 @@ export class Logger {
123
86
  this.logVerbose('AI Response Received', {
124
87
  provider,
125
88
  response: this.sanitizeForLogging(response),
126
- metadata: metadata || {},
89
+ metadata: metadata ?? {},
127
90
  timestamp: new Date().toISOString(),
128
91
  });
129
92
  }
@@ -138,7 +101,7 @@ export class Logger {
138
101
  request: this.sanitizeForLogging(request),
139
102
  response: this.sanitizeForLogging(response),
140
103
  duration: duration ? `${duration}ms` : undefined,
141
- metadata: metadata || {},
104
+ metadata: metadata ?? {},
142
105
  timestamp: new Date().toISOString(),
143
106
  });
144
107
  }
@@ -149,43 +112,35 @@ export class Logger {
149
112
  if (data === null || data === undefined) {
150
113
  return data;
151
114
  }
152
- // Handle circular references and large objects
153
115
  const seen = new WeakSet();
154
116
  const sanitize = (obj, depth = 0) => {
155
- // Prevent infinite recursion
156
117
  if (depth > 10) {
157
118
  return '[Max Depth Reached]';
158
119
  }
159
- // Handle primitives
160
120
  if (obj === null || obj === undefined) {
161
121
  return obj;
162
122
  }
163
123
  if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
164
124
  return obj;
165
125
  }
166
- // Handle circular references
167
126
  if (typeof obj === 'object') {
168
127
  if (seen.has(obj)) {
169
128
  return '[Circular Reference]';
170
129
  }
171
130
  seen.add(obj);
172
- // Handle arrays
173
131
  if (Array.isArray(obj)) {
174
132
  return obj.map((item) => sanitize(item, depth + 1));
175
133
  }
176
- // Handle Buffer (common in file uploads)
177
134
  if (Buffer.isBuffer(obj)) {
178
135
  return `[Buffer: ${obj.length} bytes]`;
179
136
  }
180
- // Handle objects
181
137
  const result = {};
182
138
  for (const key in obj) {
183
139
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
184
- // Skip sensitive keys (can be extended)
185
140
  if (key.toLowerCase().includes('password') ||
186
141
  key.toLowerCase().includes('secret') ||
187
142
  key.toLowerCase().includes('apikey') ||
188
- key.toLowerCase().includes('token') && key !== 'token') {
143
+ (key.toLowerCase().includes('token') && key !== 'token')) {
189
144
  result[key] = '[REDACTED]';
190
145
  }
191
146
  else {
@@ -200,7 +155,6 @@ export class Logger {
200
155
  return sanitize(data);
201
156
  }
202
157
  }
203
- // Default logger instance
204
158
  let defaultLogger = null;
205
159
  /**
206
160
  * Get or create the default logger instance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-providers-router",
3
- "version": "4.8.5",
3
+ "version": "4.8.6",
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.3.6",
49
+ "@x12i/logxer": "^4.4.0",
50
50
  "ai-io-normalizer": "^6.0.3"
51
51
  },
52
52
  "devDependencies": {