@x12i/ai-providers-router 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.metadata/anthropic.response-map.json +1 -0
  2. package/.metadata/google.response-map.json +1 -0
  3. package/.metadata/groq.response-map.json +1 -0
  4. package/.metadata/llm-request-config-registry.json +111 -0
  5. package/.metadata/llm-response-config-registry.json +1 -0
  6. package/.metadata/model-aliases.json +1 -0
  7. package/.metadata/model-normalization.json +1 -0
  8. package/.metadata/moonshot.response-map.json +1 -0
  9. package/.metadata/openai.response-map.json +1 -0
  10. package/.metadata/openrouter_catalog_with_vendor_mapping.json +15781 -0
  11. package/.metadata/reasoning-support.json +159 -0
  12. package/.metadata/xai.response-map.json +1 -0
  13. package/README.md +480 -0
  14. package/dist/adapters/grok/GrokAdapter.d.ts +50 -0
  15. package/dist/adapters/grok/GrokAdapter.js +342 -0
  16. package/dist/adapters/openai/OpenAIAdapter.d.ts +50 -0
  17. package/dist/adapters/openai/OpenAIAdapter.js +401 -0
  18. package/dist/adapters/openrouter/OpenRouterAdapter.d.ts +87 -0
  19. package/dist/adapters/openrouter/OpenRouterAdapter.js +1449 -0
  20. package/dist/adapters/openrouter/reasoning-capabilities.d.ts +26 -0
  21. package/dist/adapters/openrouter/reasoning-capabilities.js +79 -0
  22. package/dist/discovery.d.ts +6 -0
  23. package/dist/discovery.js +114 -0
  24. package/dist/errors.d.ts +27 -0
  25. package/dist/errors.js +33 -0
  26. package/dist/factory.d.ts +15 -0
  27. package/dist/factory.js +206 -0
  28. package/dist/gateway.d.ts +22 -0
  29. package/dist/gateway.js +154 -0
  30. package/dist/index.d.ts +9 -0
  31. package/dist/index.js +42 -0
  32. package/dist/interceptors.d.ts +10 -0
  33. package/dist/interceptors.js +1 -0
  34. package/dist/logger.d.ts +70 -0
  35. package/dist/logger.js +222 -0
  36. package/dist/openrouter-catalog.d.ts +119 -0
  37. package/dist/openrouter-catalog.js +222 -0
  38. package/dist/providers/OpenRouterProvider.d.ts +16 -0
  39. package/dist/providers/OpenRouterProvider.js +171 -0
  40. package/dist/registry/AdapterRegistry.d.ts +86 -0
  41. package/dist/registry/AdapterRegistry.js +36 -0
  42. package/dist/registry/ProviderRegistry.d.ts +24 -0
  43. package/dist/registry/ProviderRegistry.js +46 -0
  44. package/dist/router/Router.d.ts +33 -0
  45. package/dist/router/Router.js +258 -0
  46. package/dist/router/RouterTypes.d.ts +138 -0
  47. package/dist/router/RouterTypes.js +5 -0
  48. package/dist/router/RouterWrapper.d.ts +83 -0
  49. package/dist/router/RouterWrapper.js +744 -0
  50. package/dist/router.d.ts +13 -0
  51. package/dist/router.js +8 -0
  52. package/dist/types.d.ts +33 -0
  53. package/dist/types.js +1 -0
  54. package/dist/utils/esm-compat.d.ts +9 -0
  55. package/dist/utils/esm-compat.js +13 -0
  56. package/dist/utils/ids.d.ts +4 -0
  57. package/dist/utils/ids.js +6 -0
  58. package/package.json +66 -0
@@ -0,0 +1,159 @@
1
+ {
2
+ "schemaVersion": "2025-12-30",
3
+ "purpose": "Router-owned registry of reasoning-capable model families (cross-vendor) with actionable request/response handling rules",
4
+ "notes": [
5
+ "Do NOT assume reasoning support by vendor alone; match specific model patterns/families and keep this registry the source of truth.",
6
+ "OpenRouter exposes reasoning via `reasoning_details` across multiple vendors (OpenAI, Anthropic, Gemini, xAI).",
7
+ "When continuing a conversation (especially with Gemini), preserve full `reasoning_details` in follow-up turns or you may get provider errors."
8
+ ],
9
+ "reasoningDetails": {
10
+ "standardField": "reasoning_details",
11
+ "nonStreamingLocations": [
12
+ "rawResponse.output[] (Responses-like shape)",
13
+ "rawResponse.choices[].message.reasoning_details[] (Chat Completions shape)"
14
+ ],
15
+ "streamingLocations": [
16
+ "rawResponse.choices[].delta.reasoning_details[]"
17
+ ],
18
+ "supportedItemTypes": [
19
+ "reasoning.summary",
20
+ "reasoning.text",
21
+ "reasoning.encrypted"
22
+ ]
23
+ },
24
+ "requestParamNormalization": {
25
+ "openaiStyle": {
26
+ "path": "reasoning.effort",
27
+ "values": ["low", "medium", "high"],
28
+ "mutualExclusion": ["reasoning.max_tokens"]
29
+ },
30
+ "anthropicStyle": {
31
+ "path": "reasoning.max_tokens",
32
+ "type": "integer",
33
+ "mutualExclusion": ["reasoning.effort"]
34
+ }
35
+ },
36
+ "vendors": [
37
+ {
38
+ "vendorId": "openai",
39
+ "displayName": "OpenAI (via OpenRouter)",
40
+ "supportsReasoningDetails": true,
41
+ "requestModes": ["effort"],
42
+ "requestMapping": {
43
+ "settableParams": ["reasoning.effort"],
44
+ "doNotSetTogether": ["reasoning.max_tokens"]
45
+ },
46
+ "visibilitySupport": {
47
+ "summary": true,
48
+ "text": true,
49
+ "encrypted": true
50
+ },
51
+ "modelMatchRules": [
52
+ {
53
+ "ruleId": "openai-reasoning-family",
54
+ "matchType": "prefix",
55
+ "pattern": "openai/o",
56
+ "examples": ["openai/o1", "openai/o3"],
57
+ "confidence": "high"
58
+ },
59
+ {
60
+ "ruleId": "openai-gpt5-family",
61
+ "matchType": "prefix",
62
+ "pattern": "openai/gpt-5",
63
+ "examples": ["openai/gpt-5-nano", "openai/gpt-5-mini", "openai/gpt-5"],
64
+ "confidence": "medium",
65
+ "notes": [
66
+ "Treat GPT-5 family as reasoning-details capable via OpenRouter. Keeps router behavior consistent and avoids catalog-fallback false negatives."
67
+ ]
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "vendorId": "xai",
73
+ "displayName": "xAI (via OpenRouter)",
74
+ "supportsReasoningDetails": true,
75
+ "requestModes": ["effort"],
76
+ "requestMapping": {
77
+ "settableParams": ["reasoning.effort"],
78
+ "doNotSetTogether": ["reasoning.max_tokens"]
79
+ },
80
+ "visibilitySupport": {
81
+ "summary": true,
82
+ "text": true,
83
+ "encrypted": true
84
+ },
85
+ "modelMatchRules": [
86
+ {
87
+ "ruleId": "xai-reasoning-family",
88
+ "matchType": "prefix",
89
+ "pattern": "x-ai/grok",
90
+ "examples": ["x-ai/grok-4.1-fast"],
91
+ "confidence": "medium"
92
+ }
93
+ ]
94
+ },
95
+ {
96
+ "vendorId": "anthropic",
97
+ "displayName": "Anthropic (via OpenRouter)",
98
+ "supportsReasoningDetails": true,
99
+ "requestModes": ["max_tokens"],
100
+ "requestMapping": {
101
+ "settableParams": ["reasoning.max_tokens"],
102
+ "doNotSetTogether": ["reasoning.effort"]
103
+ },
104
+ "visibilitySupport": {
105
+ "summary": true,
106
+ "text": true,
107
+ "encrypted": true
108
+ },
109
+ "modelMatchRules": [
110
+ {
111
+ "ruleId": "anthropic-reasoning-family",
112
+ "matchType": "prefix",
113
+ "pattern": "anthropic/claude",
114
+ "examples": ["anthropic/claude-sonnet-4", "anthropic/claude-opus-4.5"],
115
+ "confidence": "medium",
116
+ "notes": [
117
+ "Not every Claude model is necessarily reasoning-enabled; keep this rule scoped to the specific slugs you validate in fixtures."
118
+ ]
119
+ }
120
+ ]
121
+ },
122
+ {
123
+ "vendorId": "google",
124
+ "displayName": "Google Gemini (via OpenRouter)",
125
+ "supportsReasoningDetails": true,
126
+ "requestModes": ["max_tokens"],
127
+ "requestMapping": {
128
+ "settableParams": ["reasoning.max_tokens"],
129
+ "doNotSetTogether": ["reasoning.effort"]
130
+ },
131
+ "visibilitySupport": {
132
+ "summary": true,
133
+ "text": true,
134
+ "encrypted": true
135
+ },
136
+ "conversationRequirements": [
137
+ {
138
+ "requirementId": "preserve-reasoning-details",
139
+ "description": "When sending the next turn / tool follow-up, preserve the complete `reasoning_details` blocks in the messages you pass back, or Gemini models may reject the request.",
140
+ "appliesTo": ["google/gemini-*"],
141
+ "severity": "error-prone"
142
+ }
143
+ ],
144
+ "modelMatchRules": [
145
+ {
146
+ "ruleId": "gemini-reasoning-family",
147
+ "matchType": "prefix",
148
+ "pattern": "google/gemini",
149
+ "examples": ["google/gemini-3-pro-preview"],
150
+ "confidence": "medium",
151
+ "notes": [
152
+ "Gemini has both non-reasoning and reasoning variants; validate per-model in your own fixtures and tighten patterns accordingly."
153
+ ]
154
+ }
155
+ ]
156
+ }
157
+ ]
158
+ }
159
+
@@ -0,0 +1 @@
1
+ {}
package/README.md ADDED
@@ -0,0 +1,480 @@
1
+ # @x12i/ai-providers-router
2
+
3
+ A unified **LLM provider router** that routes requests to installed provider packages using the **ProviderModule architecture**.
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
13
+
14
+ ## Architecture
15
+
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)
20
+
21
+ > Important: This router **never installs** provider packages at runtime.
22
+
23
+ ---
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm i @x12i/ai-providers-router
29
+ ```
30
+
31
+ Install at least one provider package (examples):
32
+
33
+ ```bash
34
+ npm i @x12i/ai-provider-openai
35
+ npm i @x12i/ai-provider-anthropic
36
+ npm i @x12i/ai-provider-google
37
+ npm i @x12i/ai-provider-xai
38
+ npm i @x12i/ai-provider-groq
39
+ ```
40
+
41
+ **For OpenRouter mode**: Only `@x12i/ai-provider-openai` is required to access **353 models from 67 providers** through OpenRouter's unified API.
42
+
43
+ ---
44
+
45
+ ## Provider IDs (canonical)
46
+
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)
54
+
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
59
+
60
+ > Grok ≠ Groq
61
+ >
62
+ > * Grok is **xAI** (`xai`)
63
+ > * Groq is **GroqCloud** (`groq`)
64
+
65
+ ---
66
+
67
+ ## OpenRouter Mode
68
+
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.
70
+
71
+ ### Key Features
72
+
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)
82
+
83
+ ### OpenRouter Mode - Completely Automatic
84
+
85
+ **OpenRouter mode works automatically - no code changes required!**
86
+
87
+ Simply set the `OPEN_ROUTER_KEY` environment variable:
88
+
89
+ ```bash
90
+ export OPEN_ROUTER_KEY=sk-or-your-openrouter-api-key-here
91
+ ```
92
+
93
+ That's it! OpenRouter mode is **completely automatic** and works with:
94
+
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
98
+
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
104
+
105
+ **To disable OpenRouter mode explicitly:**
106
+
107
+ ```bash
108
+ export USE_OPENROUTER=false
109
+ ```
110
+
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`).
112
+
113
+ **Troubleshooting:**
114
+
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
120
+
121
+ The router will automatically use OpenRouter mode when these conditions are met!
122
+
123
+ ### Usage Examples
124
+
125
+ **Example 1: Using provider names (seamless - no code changes needed):**
126
+
127
+ ```ts
128
+ const router = await createRouter();
129
+
130
+ // Works exactly the same whether OpenRouter mode is on or off
131
+ 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
137
+ mode: 'sync',
138
+ };
139
+
140
+ const res = await router.invoke(req);
141
+ // Model automatically mapped to "openai/gpt-4o" when using OpenRouter
142
+ ```
143
+
144
+ **Example 2: Provider inference (no provider specified):**
145
+
146
+ ```ts
147
+ // Router infers provider from model name
148
+ const req: AIRouterRequest = {
149
+ request: {
150
+ messages: [{ role: 'user', content: 'Hello!' }],
151
+ config: { model: 'gpt-4o' }, // Infers "openai" from "gpt-4o"
152
+ },
153
+ // provider not specified - router infers "openai"
154
+ mode: 'sync',
155
+ };
156
+
157
+ const res = await router.invoke(req);
158
+ ```
159
+
160
+ **Example 3: Using OpenRouter model format directly:**
161
+
162
+ ```ts
163
+ // Call any OpenRouter-supported model using OpenRouter's format
164
+ 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
170
+ mode: 'sync',
171
+ };
172
+
173
+ const res = await router.invoke(req);
174
+ ```
175
+
176
+ **Example 4: Accessing models without provider packages:**
177
+
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
+ };
188
+
189
+ const res = await router.invoke(req);
190
+ ```
191
+
192
+ **Example 5: Using diverse models from different providers:**
193
+
194
+ ```ts
195
+ // Anthropic Claude models
196
+ const claudeReq = { request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'claude-3-opus' } }, provider: 'anthropic', mode: 'sync' };
197
+
198
+ // Google Gemini models
199
+ const geminiReq = { request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'gemini-pro' } }, provider: 'google', mode: 'sync' };
200
+
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' };
203
+
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
+ ]);
210
+ ```
211
+
212
+ ### How OpenRouter Mode Works
213
+
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
218
+
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
223
+
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
229
+
230
+ ### Provider Inference Rules
231
+
232
+ When no provider is specified, the router uses **catalog data** to intelligently infer providers from model names. This includes:
233
+
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:
238
+
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)
245
+
246
+ ### Model Validation & Catalog Features
247
+
248
+ The router automatically validates models against the OpenRouter catalog:
249
+
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
257
+
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
263
+
264
+ ### OpenRouter Configuration
265
+
266
+ Optional environment variables for OpenRouter rankings:
267
+
268
+ ```bash
269
+ export OPEN_ROUTER_HTTP_REFERER=https://your-site.com
270
+ export OPEN_ROUTER_X_TITLE=Your Site Name
271
+ ```
272
+
273
+ See [Environment Variables documentation](./docs/environment-variables.md) for details.
274
+
275
+ ---
276
+
277
+ ## Zero-config router creation
278
+
279
+ No arguments are required.
280
+
281
+ ```ts
282
+ import { createRouter } from '@x12i/ai-providers-router';
283
+
284
+ const router = await createRouter();
285
+ ```
286
+
287
+ Optional router-level config (logging, usage tracking, timeout):
288
+
289
+ ```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
+ },
297
+ });
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Request/Response Types
303
+
304
+ Router uses its own request/response types:
305
+
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)
310
+
311
+ ```ts
312
+ import type { AIRouterRequest, AIResponse } from '@x12i/ai-providers-router';
313
+
314
+ // Request reasoning with extended effort levels
315
+ config: {
316
+ reasoning: {
317
+ effort: 'high', // or 'low', 'medium', 'high', 'xhigh' (xhigh normalized to high)
318
+ maxTokens: 2000, // optional: for Anthropic/Gemini models (max_tokens mode)
319
+ visibility: 'trace', // or 'none', 'summary' (best-effort; downgraded if not returned)
320
+ onUnsupported: 'downgrade' // or 'error' (throws), 'ignore' (silent)
321
+ }
322
+ }
323
+
324
+ // Access unified reasoning response
325
+ response.reasoning.artifacts.encrypted // Encrypted reasoning traces
326
+ response.reasoning.applied.effort // What was actually applied (may differ from requested)
327
+ response.reasoning.applied.visibility // What visibility was actually returned
328
+ response.reasoning.availability // Model capability flags
329
+ response.reasoning.warnings // Any downgrade/normalization warnings
330
+ ```
331
+
332
+ **Reasoning Features:**
333
+ - ✅ **Effort Control**: `low`, `medium`, `high`, `xhigh` (xhigh auto-normalized to high)
334
+ - ✅ **Max Tokens Control**: Direct `maxTokens` budget for Anthropic/Gemini models
335
+ - ✅ **Encrypted Traces**: Access encrypted reasoning artifacts (ciphertext not decryptable by user; only metadata/prefix logged)
336
+ - ✅ **Summary Visibility**: Human-readable reasoning summary (best-effort; returned only if provider returns `reasoning_details` with `reasoning.summary`; otherwise downgraded with warning)
337
+ - ✅ **Trace Visibility**: Encrypted or readable reasoning traces (best-effort; satisfied by either `reasoning.encrypted` artifacts or `reasoning.text` chunks; downgraded if not available)
338
+ - ✅ **Model Detection**: Automatic detection of reasoning-capable models via JSON registry (cross-vendor support)
339
+ - ✅ **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
340
+
341
+ **Supported Models**: Currently detected via router-owned JSON registry (`.metadata/reasoning-support.json`):
342
+ - **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`
343
+ - **xAI Grok** (`x-ai/grok*` pattern): `x-ai/grok-4.1-fast` and other reasoning-enabled Grok models
344
+ - **Anthropic Claude** (`anthropic/claude*` pattern): Reasoning-enabled Claude models (uses `max_tokens` mode)
345
+ - **Google Gemini** (`google/gemini*` pattern): Reasoning-enabled Gemini models (uses `max_tokens` mode)
346
+
347
+ > ℹ️ **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.
348
+
349
+ See [Reasoning Integration Guide](./docs/reasoning-integration.md) and [Reasoning Supported Models](./docs/reasoning-supported-models.md) for complete documentation.
350
+
351
+ ---
352
+
353
+ ## Sync call
354
+
355
+ ```ts
356
+ import { createRouter, type AIRouterRequest, type AIResponse } from '@x12i/ai-providers-router';
357
+
358
+ const router = await createRouter();
359
+
360
+ const req: AIRouterRequest = {
361
+ request: {
362
+ inputData: 'Write 3 bullets about routers.',
363
+ config: {
364
+ maxTokens: 200,
365
+ temperature: 0.7,
366
+ model: 'gpt-4o-mini',
367
+ },
368
+ },
369
+ provider: 'openai',
370
+ mode: 'sync',
371
+ exec: {
372
+ timeoutMs: 60000, // Optional: override default timeout
373
+ idempotencyKey: 'optional-key', // Optional: for idempotent requests
374
+ },
375
+ };
376
+
377
+ const res: AIResponse = await router.invoke(req);
378
+
379
+ console.log(res.outputText); // Normalized text (optional)
380
+ console.log(res.rawResponse); // Lossless raw response (always present)
381
+ console.log(res.usage); // Token usage
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Streaming call
387
+
388
+ ```ts
389
+ const streamReq: AIRouterRequest = {
390
+ ...req,
391
+ mode: 'stream',
392
+ };
393
+
394
+ for await (const ev of router.stream(streamReq)) {
395
+ if (ev.type === 'provider_raw') {
396
+ // Raw provider event (always emitted for debugging)
397
+ console.log('Raw event:', ev.raw);
398
+ } else if (ev.type === 'output_text_delta') {
399
+ // Normalized text delta
400
+ process.stdout.write(ev.delta);
401
+ } else if (ev.type === 'completed') {
402
+ // Final response
403
+ console.log('Final:', ev.response.outputText);
404
+ } else if (ev.type === 'error') {
405
+ console.error('Error:', ev.error);
406
+ }
407
+ }
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Batch requests
413
+
414
+ Batch requests use the batch API (gated by ProviderModule capabilities):
415
+
416
+ ```ts
417
+ const items = [
418
+ { request: { inputData: 'First request', config: { model: 'gpt-4o-mini' } } },
419
+ { request: { inputData: 'Second request', config: { model: 'gpt-4o-mini' } } },
420
+ ];
421
+
422
+ const batchResult = await router.createBatch('openai', items, {
423
+ timeoutMs: 120000, // Optional: override default timeout
424
+ idempotencyKey: 'optional-key', // Optional
425
+ });
426
+
427
+ console.log(batchResult.items); // Array of results
428
+ console.log(batchResult.rawBatch); // Lossless raw batch response
429
+ ```
430
+
431
+ **Note**: Batch is only available if `provider.capabilities.modes.batch === true`. Router gates execution by ProviderModule capabilities, not transformer supports.
432
+
433
+ ---
434
+
435
+ ## How it works (high level)
436
+
437
+ 1. Router receives an `AIRouterRequest`
438
+ 2. **Request Interceptors** (if OpenRouter mode enabled):
439
+ - Preserve original provider name for model mapping
440
+ - Route requests to OpenRouter provider
441
+ - Infer provider from model name if not specified
442
+ 3. Router loads ProviderModule from installed provider package (lazy import)
443
+ 4. Router checks `provider.capabilities.modes` to gate execution
444
+ 5. Router-side adapter converts request to `ProviderSDKCallSpec`
445
+ - **OpenRouterAdapter**: Maps provider + model to OpenRouter format (e.g., `"openai/gpt-4o"`)
446
+ 6. Router calls ProviderModule:
447
+
448
+ * `provider.execute(spec)` (sync)
449
+ * `provider.stream(spec)` (streaming)
450
+ * `provider.submitBatch(specs)` (batch)
451
+ 7. Router-side adapter parses `ProviderSDKExecResult` to `AIResponse`
452
+ - **OpenRouterAdapter**: Parses OpenAI Chat Completions format directly (no ai-io-normalizer)
453
+ 8. Router returns standardized response with lossless `rawResponse`
454
+
455
+ ---
456
+
457
+ ## Provider packages are required
458
+
459
+ If you call a provider that is not installed, the router throws a clear error with install instructions.
460
+
461
+ **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.
462
+
463
+ **Supported Providers in OpenRouter Mode:**
464
+ - All major providers: OpenAI, Anthropic, Google, xAI (Grok), Groq, Meta, Mistral, Cohere, etc.
465
+ - 67 total providers from the OpenRouter catalog
466
+ - 353 models with full capability support
467
+
468
+ Examples:
469
+
470
+ * Provider `openai` requires `@x12i/ai-provider-openai`
471
+ * Provider `grok` requires `@x12i/ai-provider-grok`
472
+ * **OpenRouter mode**: Only requires `@x12i/ai-provider-openai` to access all OpenRouter-supported models
473
+
474
+ This router will never auto-install packages.
475
+
476
+ ---
477
+
478
+ ## License
479
+
480
+ ISC
@@ -0,0 +1,50 @@
1
+ import type { ProviderSDKCallSpec, ProviderSDKExecResult, ProviderSDKStreamChunk, ProviderBatchResults } from '@x12i/ai-provider-interface';
2
+ import type { RouterAdapter } from '../../registry/AdapterRegistry.js';
3
+ import type { AIResponse, AIStreamEvent } from '../../router/RouterTypes.js';
4
+ /**
5
+ * Router-side adapter for Grok/xAI provider
6
+ * Converts router requests to ProviderSDKCallSpec and parses responses
7
+ */
8
+ export declare class GrokAdapter implements RouterAdapter {
9
+ provider: string;
10
+ buildCallSpec(input: {
11
+ requestId: string;
12
+ mode: 'sync' | 'stream';
13
+ request: any;
14
+ exec?: {
15
+ timeoutMs?: number;
16
+ retries?: number;
17
+ idempotencyKey?: string;
18
+ signal?: AbortSignal;
19
+ };
20
+ }): Promise<ProviderSDKCallSpec>;
21
+ parseResponse(input: {
22
+ requestId: string;
23
+ request: any;
24
+ execResult: ProviderSDKExecResult;
25
+ }): AIResponse;
26
+ parseStreamChunk(input: {
27
+ requestId: string;
28
+ request: any;
29
+ chunk: ProviderSDKStreamChunk;
30
+ }): AIStreamEvent[];
31
+ finalizeStream(input: {
32
+ requestId: string;
33
+ request: any;
34
+ collected: {
35
+ rawEvents: unknown[];
36
+ outputText?: string;
37
+ };
38
+ finalRaw?: unknown;
39
+ }): AIResponse;
40
+ parseBatchItem(input: {
41
+ requestId: string;
42
+ request: any;
43
+ item: ProviderBatchResults['items'][number];
44
+ }): {
45
+ requestId: string;
46
+ rawResponse?: unknown;
47
+ outputText?: string;
48
+ error?: any;
49
+ };
50
+ }