@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.
- package/.metadata/anthropic.response-map.json +1 -0
- package/.metadata/google.response-map.json +1 -0
- package/.metadata/groq.response-map.json +1 -0
- package/.metadata/llm-request-config-registry.json +111 -0
- package/.metadata/llm-response-config-registry.json +1 -0
- package/.metadata/model-aliases.json +1 -0
- package/.metadata/model-normalization.json +1 -0
- package/.metadata/moonshot.response-map.json +1 -0
- package/.metadata/openai.response-map.json +1 -0
- package/.metadata/openrouter_catalog_with_vendor_mapping.json +15781 -0
- package/.metadata/reasoning-support.json +159 -0
- package/.metadata/xai.response-map.json +1 -0
- package/README.md +480 -0
- package/dist/adapters/grok/GrokAdapter.d.ts +50 -0
- package/dist/adapters/grok/GrokAdapter.js +342 -0
- package/dist/adapters/openai/OpenAIAdapter.d.ts +50 -0
- package/dist/adapters/openai/OpenAIAdapter.js +401 -0
- package/dist/adapters/openrouter/OpenRouterAdapter.d.ts +87 -0
- package/dist/adapters/openrouter/OpenRouterAdapter.js +1449 -0
- package/dist/adapters/openrouter/reasoning-capabilities.d.ts +26 -0
- package/dist/adapters/openrouter/reasoning-capabilities.js +79 -0
- package/dist/discovery.d.ts +6 -0
- package/dist/discovery.js +114 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +33 -0
- package/dist/factory.d.ts +15 -0
- package/dist/factory.js +206 -0
- package/dist/gateway.d.ts +22 -0
- package/dist/gateway.js +154 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +42 -0
- package/dist/interceptors.d.ts +10 -0
- package/dist/interceptors.js +1 -0
- package/dist/logger.d.ts +70 -0
- package/dist/logger.js +222 -0
- package/dist/openrouter-catalog.d.ts +119 -0
- package/dist/openrouter-catalog.js +222 -0
- package/dist/providers/OpenRouterProvider.d.ts +16 -0
- package/dist/providers/OpenRouterProvider.js +171 -0
- package/dist/registry/AdapterRegistry.d.ts +86 -0
- package/dist/registry/AdapterRegistry.js +36 -0
- package/dist/registry/ProviderRegistry.d.ts +24 -0
- package/dist/registry/ProviderRegistry.js +46 -0
- package/dist/router/Router.d.ts +33 -0
- package/dist/router/Router.js +258 -0
- package/dist/router/RouterTypes.d.ts +138 -0
- package/dist/router/RouterTypes.js +5 -0
- package/dist/router/RouterWrapper.d.ts +83 -0
- package/dist/router/RouterWrapper.js +744 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/dist/utils/esm-compat.d.ts +9 -0
- package/dist/utils/esm-compat.js +13 -0
- package/dist/utils/ids.d.ts +4 -0
- package/dist/utils/ids.js +6 -0
- 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
|
+
}
|