@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,258 @@
1
+ import { newId } from '../utils/ids.js';
2
+ /**
3
+ * Main router class
4
+ * Orchestrates provider execution using ProviderModules and router-side adapters
5
+ */
6
+ export class AIRouter {
7
+ constructor(providers, adapters) {
8
+ this.providers = providers;
9
+ this.adapters = adapters;
10
+ }
11
+ /**
12
+ * Resolve provider name from request, checking OpenRouter mode first
13
+ */
14
+ resolveProviderName(input) {
15
+ const hasOpenRouterAdapter = this.adapters.has('openrouter');
16
+ const hasOpenRouterProvider = this.providers.has('openrouter');
17
+ // Check if OPEN_ROUTER_KEY is set in environment (completely automatic detection)
18
+ const hasOpenRouterKey = !!(process.env.OPEN_ROUTER_KEY &&
19
+ process.env.OPEN_ROUTER_KEY.trim() !== '' &&
20
+ !process.env.OPEN_ROUTER_KEY.startsWith('ENV.'));
21
+ // Normalize config.provider (handle string, trim whitespace)
22
+ const cfgProviderRaw = input.request?.config?.provider;
23
+ const cfgProvider = typeof cfgProviderRaw === 'string' ? cfgProviderRaw.trim() : undefined;
24
+ const hasProviderInConfig = !!cfgProvider && cfgProvider !== '';
25
+ const hasProviderAtTopLevel = input.provider === 'openrouter';
26
+ const hasRegisteredProviders = this.providers.list().length > 0;
27
+ // OpenRouter mode detection (COMPLETELY AUTOMATIC):
28
+ // 1. Explicit: top-level provider is 'openrouter'
29
+ // 2. OpenRouter provider is registered (when OPEN_ROUTER_KEY is set via factory)
30
+ // 3. AUTOMATIC: OpenRouter adapter exists + OPEN_ROUTER_KEY env var is set + config.provider is specified
31
+ // This works even when router is created manually without calling createRouter()
32
+ // 4. Fallback: adapter exists + config.provider + no providers registered (auto-detect)
33
+ //
34
+ // IMPORTANT: When OpenRouter provider is registered OR OPEN_ROUTER_KEY is set, OpenRouter mode is active.
35
+ // All provider names (openai, grok, anthropic, etc.) route through OpenRouter.
36
+ // The interceptor sets input.provider = 'openrouter' when OpenRouter mode is enabled.
37
+ const isOpenRouterMode = hasProviderAtTopLevel ||
38
+ hasOpenRouterProvider || // OpenRouter provider registered = OpenRouter mode is DEFAULT
39
+ (hasOpenRouterAdapter && hasOpenRouterKey && hasProviderInConfig) || // AUTOMATIC: env var + adapter + config.provider
40
+ (hasOpenRouterAdapter && hasProviderInConfig && !hasRegisteredProviders); // Fallback: adapter + config.provider + no providers
41
+ if (isOpenRouterMode) {
42
+ return 'openrouter';
43
+ }
44
+ // Honor request.config.provider in normal flow
45
+ if (hasProviderInConfig) {
46
+ // Validate provider is registered
47
+ if (!this.providers.has(cfgProvider)) {
48
+ const available = this.providers.list().join(', ');
49
+ // Check if OpenRouter mode could work automatically
50
+ if (hasOpenRouterAdapter && !hasOpenRouterKey) {
51
+ throw new Error(`Provider '${cfgProvider}' specified in request.config.provider but not registered. ` +
52
+ `OpenRouter adapter is available - set OPEN_ROUTER_KEY environment variable to enable automatic OpenRouter mode. ` +
53
+ `Available providers: ${available || '(none)'}`);
54
+ }
55
+ throw new Error(`Provider '${cfgProvider}' specified in request.config.provider but not registered. Available providers: ${available || '(none)'}`);
56
+ }
57
+ // Validate adapter is registered
58
+ if (!this.adapters.has(cfgProvider)) {
59
+ const available = this.adapters.list().join(', ');
60
+ throw new Error(`Adapter '${cfgProvider}' specified in request.config.provider but not registered. Available adapters: ${available || '(none)'}`);
61
+ }
62
+ return cfgProvider;
63
+ }
64
+ // Existing fallback: top-level provider or first registered provider
65
+ const providerName = input.provider ?? this.providers.list()[0];
66
+ if (!providerName) {
67
+ // Improved error messages with OpenRouter mode guidance
68
+ if (hasProviderInConfig && hasOpenRouterAdapter && !hasRegisteredProviders) {
69
+ if (!hasOpenRouterKey) {
70
+ throw new Error('OpenRouter adapter available and config.provider specified, but OpenRouter provider module not registered. ' +
71
+ 'Set OPEN_ROUTER_KEY environment variable to enable automatic OpenRouter mode (works with both createRouter() and manual initialization).');
72
+ }
73
+ throw new Error('OpenRouter adapter available and config.provider specified, but OpenRouter provider module not registered. ' +
74
+ 'OPEN_ROUTER_KEY is set but provider module initialization failed. Check that @x12i/ai-provider-openai is installed.');
75
+ }
76
+ if (hasProviderInConfig && !hasOpenRouterAdapter) {
77
+ throw new Error(`Provider '${input.request?.config?.provider}' specified in config but no providers registered and OpenRouter adapter not available.`);
78
+ }
79
+ // Final fallback error with OpenRouter suggestion
80
+ if (hasOpenRouterAdapter && !hasOpenRouterKey) {
81
+ throw new Error('No provider specified and no providers registered. ' +
82
+ 'OpenRouter adapter is available - set OPEN_ROUTER_KEY environment variable to enable automatic OpenRouter mode.');
83
+ }
84
+ throw new Error('No provider specified and no providers registered');
85
+ }
86
+ return providerName;
87
+ }
88
+ /**
89
+ * Execute a sync request
90
+ */
91
+ async runSync(input) {
92
+ const requestId = input.requestId ?? newId();
93
+ const providerName = this.resolveProviderName(input);
94
+ const provider = this.providers.get(providerName);
95
+ const adapter = this.adapters.get(providerName);
96
+ // Check capabilities
97
+ if (!provider.capabilities.modes.sync) {
98
+ throw new Error(`Provider '${providerName}' does not support sync mode`);
99
+ }
100
+ // Build call spec
101
+ const spec = await adapter.buildCallSpec({
102
+ requestId,
103
+ mode: 'sync',
104
+ request: input.request,
105
+ exec: input.exec,
106
+ });
107
+ // Execute
108
+ const execResult = await provider.execute(spec);
109
+ // Parse response
110
+ return adapter.parseResponse({
111
+ requestId,
112
+ request: { ...input.request, _callSpec: spec }, // Include call spec for adapter access
113
+ execResult,
114
+ });
115
+ }
116
+ /**
117
+ * Execute a streaming request
118
+ */
119
+ async *runStream(input) {
120
+ const requestId = input.requestId ?? newId();
121
+ const providerName = this.resolveProviderName(input);
122
+ const provider = this.providers.get(providerName);
123
+ const adapter = this.adapters.get(providerName);
124
+ // Check capabilities
125
+ if (!provider.capabilities.modes.stream) {
126
+ throw new Error(`Provider '${providerName}' does not support stream mode`);
127
+ }
128
+ if (!provider.stream) {
129
+ throw new Error(`Provider '${providerName}' does not implement stream()`);
130
+ }
131
+ if (!adapter.parseStreamChunk || !adapter.finalizeStream) {
132
+ throw new Error(`Adapter for '${providerName}' does not support streaming`);
133
+ }
134
+ // Build call spec
135
+ const spec = await adapter.buildCallSpec({
136
+ requestId,
137
+ mode: 'stream',
138
+ request: input.request,
139
+ exec: input.exec,
140
+ });
141
+ // Collect stream data
142
+ const collected = {
143
+ rawEvents: [],
144
+ outputText: '',
145
+ };
146
+ try {
147
+ // Stream chunks
148
+ for await (const chunk of provider.stream(spec)) {
149
+ collected.rawEvents.push(chunk.raw);
150
+ // Emit provider_raw event
151
+ yield {
152
+ type: 'provider_raw',
153
+ requestId,
154
+ provider: providerName,
155
+ raw: chunk.raw,
156
+ };
157
+ // Parse chunk to router events
158
+ const events = adapter.parseStreamChunk({
159
+ requestId,
160
+ request: input.request,
161
+ chunk,
162
+ });
163
+ for (const ev of events) {
164
+ // Accumulate output text
165
+ if (ev.type === 'output_text_delta') {
166
+ collected.outputText += ev.delta;
167
+ }
168
+ yield ev;
169
+ }
170
+ }
171
+ // Finalize stream
172
+ const response = adapter.finalizeStream({
173
+ requestId,
174
+ request: input.request,
175
+ collected,
176
+ });
177
+ yield {
178
+ type: 'completed',
179
+ requestId,
180
+ response,
181
+ };
182
+ }
183
+ catch (error) {
184
+ yield {
185
+ type: 'error',
186
+ requestId,
187
+ error: error instanceof Error ? error : new Error(String(error)),
188
+ };
189
+ throw error;
190
+ }
191
+ }
192
+ /**
193
+ * Execute a batch request
194
+ */
195
+ async runBatch(providerName, items, exec) {
196
+ const provider = this.providers.get(providerName);
197
+ const adapter = this.adapters.get(providerName);
198
+ // Check capabilities
199
+ if (!provider.capabilities.modes.batch) {
200
+ throw new Error(`Provider '${providerName}' does not support batch mode`);
201
+ }
202
+ if (!provider.submitBatch || !provider.getBatchStatus || !provider.fetchBatchResults) {
203
+ throw new Error(`Provider '${providerName}' does not implement batch methods`);
204
+ }
205
+ if (!adapter.parseBatchItem) {
206
+ throw new Error(`Adapter for '${providerName}' does not support batch parsing`);
207
+ }
208
+ // Ensure exec.timeoutMs is always set (router owns execution semantics)
209
+ const execWithTimeout = {
210
+ timeoutMs: exec?.timeoutMs ?? 60000,
211
+ retries: exec?.retries,
212
+ };
213
+ // Fix: Persist requestIds for items that don't have them, so we can map results back correctly
214
+ const itemsWithIds = items.map((it) => ({
215
+ ...it,
216
+ requestId: it.requestId ?? newId(),
217
+ }));
218
+ // Build call specs for each item using persisted requestIds
219
+ const specs = await Promise.all(itemsWithIds.map((it) => adapter.buildCallSpec({
220
+ requestId: it.requestId,
221
+ mode: 'sync', // Batch items are sync specs
222
+ request: it.request,
223
+ exec: execWithTimeout,
224
+ })));
225
+ // Submit batch
226
+ const handle = await provider.submitBatch(specs);
227
+ // Poll until complete
228
+ while (true) {
229
+ const status = await provider.getBatchStatus(handle);
230
+ if (status.state === 'completed') {
231
+ break;
232
+ }
233
+ if (status.state === 'failed' || status.state === 'canceled') {
234
+ throw new Error(`Batch ended with state: ${status.state}`);
235
+ }
236
+ // Wait before polling again
237
+ await new Promise((r) => setTimeout(r, 2000));
238
+ }
239
+ // Fetch results
240
+ const batchResults = await provider.fetchBatchResults(handle);
241
+ // Fix: Use Map for efficient lookup by requestId
242
+ const originalById = new Map(itemsWithIds.map((it) => [it.requestId, it]));
243
+ // Parse each item using the Map for correct correlation
244
+ const parsedItems = batchResults.items.map((item) => {
245
+ const originalItem = originalById.get(item.requestId);
246
+ return adapter.parseBatchItem({
247
+ requestId: item.requestId,
248
+ request: originalItem?.request || {},
249
+ item,
250
+ });
251
+ });
252
+ return {
253
+ provider: providerName,
254
+ rawBatch: batchResults.rawResponse,
255
+ items: parsedItems,
256
+ };
257
+ }
258
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Router-level request and response types
3
+ * These are the public API of the router
4
+ */
5
+ export type RouterMode = 'sync' | 'stream' | 'batch';
6
+ /**
7
+ * Router configuration
8
+ */
9
+ export interface RouterConfig {
10
+ usageTracker?: {
11
+ recordRequest(event: {
12
+ provider: string;
13
+ timestamp: number;
14
+ duration: number;
15
+ tokens: {
16
+ input: number;
17
+ output: number;
18
+ total: number;
19
+ };
20
+ cost?: number;
21
+ success: boolean;
22
+ }): void;
23
+ };
24
+ verbose?: boolean;
25
+ logLevel?: 'error' | 'warn' | 'info' | 'debug' | 'verbose';
26
+ logger?: any;
27
+ timeoutMs?: number;
28
+ defaultTimeoutMs?: number;
29
+ /** OpenRouter provider config (e.g. from gateway); used when env OPEN_ROUTER_KEY is not visible */
30
+ openrouter?: {
31
+ apiKey?: string;
32
+ httpReferer?: string;
33
+ xTitle?: string;
34
+ };
35
+ }
36
+ /**
37
+ * Router request - the input to the router
38
+ */
39
+ export interface AIRouterRequest {
40
+ requestId?: string;
41
+ request: any;
42
+ provider?: string;
43
+ mode: RouterMode;
44
+ exec?: {
45
+ timeoutMs?: number;
46
+ retries?: number;
47
+ idempotencyKey?: string;
48
+ signal?: AbortSignal;
49
+ };
50
+ }
51
+ /**
52
+ * Router response for sync mode
53
+ */
54
+ export interface AIResponse {
55
+ requestId: string;
56
+ provider: string;
57
+ rawResponse: unknown;
58
+ outputText?: string;
59
+ usage?: any;
60
+ reasoning: {
61
+ requested: {
62
+ effort: string;
63
+ visibility: string;
64
+ };
65
+ applied: {
66
+ effort: string;
67
+ visibility: string;
68
+ };
69
+ artifacts: {
70
+ encrypted?: any[];
71
+ summary?: {
72
+ text: string;
73
+ format: string;
74
+ };
75
+ trace?: {
76
+ text: string;
77
+ format: string;
78
+ };
79
+ };
80
+ availability: {
81
+ supportsEffort: boolean;
82
+ supportsSummary: boolean;
83
+ supportsTrace: boolean;
84
+ supportsEncrypted: boolean;
85
+ };
86
+ warnings?: any[];
87
+ };
88
+ metadata?: any;
89
+ }
90
+ /**
91
+ * Router stream events
92
+ */
93
+ export type AIStreamEvent = {
94
+ type: 'provider_raw';
95
+ requestId: string;
96
+ provider: string;
97
+ raw: unknown;
98
+ } | {
99
+ type: 'output_text_delta';
100
+ requestId: string;
101
+ delta: string;
102
+ } | {
103
+ type: 'reasoning_summary_delta';
104
+ requestId: string;
105
+ delta: string;
106
+ } | {
107
+ type: 'reasoning_trace_delta';
108
+ requestId: string;
109
+ delta: string;
110
+ } | {
111
+ type: 'completed';
112
+ requestId: string;
113
+ response: AIResponse;
114
+ } | {
115
+ type: 'error';
116
+ requestId: string;
117
+ error: any;
118
+ };
119
+ /**
120
+ * Batch request item
121
+ */
122
+ export interface AIBatchRequestItem {
123
+ requestId?: string;
124
+ request: any;
125
+ }
126
+ /**
127
+ * Batch response
128
+ */
129
+ export interface AIBatchResponse {
130
+ provider: string;
131
+ items: Array<{
132
+ requestId: string;
133
+ rawResponse?: unknown;
134
+ outputText?: string;
135
+ error?: any;
136
+ }>;
137
+ rawBatch?: unknown;
138
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Router-level request and response types
3
+ * These are the public API of the router
4
+ */
5
+ export {};
@@ -0,0 +1,83 @@
1
+ import { ProviderRegistry } from '../registry/ProviderRegistry.js';
2
+ import { AdapterRegistry } from '../registry/AdapterRegistry.js';
3
+ import type { AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem } from './RouterTypes.js';
4
+ import type { RouterConfig } from './RouterTypes.js';
5
+ import type { RequestInterceptor, ResponseInterceptor } from '../interceptors.js';
6
+ /**
7
+ * Wrapper around AIRouter that adds logging, interceptors, and usage tracking
8
+ * Maintains backward compatibility with existing router API
9
+ */
10
+ export declare class LLMProviderRouter {
11
+ private router;
12
+ private providerRegistry;
13
+ private adapterRegistry;
14
+ private config;
15
+ private requestInterceptors;
16
+ private responseInterceptors;
17
+ private logger;
18
+ private providerConfigs;
19
+ private lastConfiguredProvider;
20
+ private autoRegistrationAttempted;
21
+ constructor(config?: RouterConfig);
22
+ /**
23
+ * Register a provider module
24
+ */
25
+ registerProvider(providerModuleOrFactory: any, factoryName?: string): void;
26
+ /**
27
+ * Configure provider SDK client config
28
+ */
29
+ configureProvider(providerName: string, config: any): void;
30
+ /**
31
+ * Add request interceptor
32
+ */
33
+ addRequestInterceptor(interceptor: RequestInterceptor): void;
34
+ /**
35
+ * Add response interceptor
36
+ */
37
+ addResponseInterceptor(interceptor: ResponseInterceptor): void;
38
+ /**
39
+ * Invoke router (sync mode)
40
+ */
41
+ invoke(request: AIRouterRequest): Promise<AIResponse>;
42
+ /**
43
+ * Stream request
44
+ */
45
+ stream(request: AIRouterRequest): AsyncIterable<AIStreamEvent>;
46
+ /**
47
+ * Batch request
48
+ */
49
+ createBatch(providerName: string, items: AIBatchRequestItem[], exec?: {
50
+ timeoutMs?: number;
51
+ retries?: number;
52
+ idempotencyKey?: string;
53
+ signal?: AbortSignal;
54
+ }): Promise<AIBatchResponse>;
55
+ /**
56
+ * List registered providers
57
+ */
58
+ listProviders(): string[];
59
+ /**
60
+ * Check health of a specific provider
61
+ */
62
+ checkHealth(provider: string): Promise<import('../router.js').HealthCheckResult>;
63
+ /**
64
+ * Get provider registry (for advanced usage)
65
+ */
66
+ getProviderRegistry(): ProviderRegistry;
67
+ /**
68
+ * Get adapter registry (for advanced usage)
69
+ */
70
+ getAdapterRegistry(): AdapterRegistry;
71
+ /**
72
+ * Try to register OpenRouter if a key is available (constructor config or resolved env).
73
+ * Used by ensureProvidersRegistered() and by the OpenRouter retry path when the first run skipped registration.
74
+ */
75
+ private tryRegisterOpenRouter;
76
+ /**
77
+ * Automatically detect and register providers based on environment variables
78
+ * This ensures "Zero-Config" initialization works even if createRouter() isn't used.
79
+ * When the first run did not register OpenRouter (e.g. key was missing), a later invoke
80
+ * can still register it by retrying OpenRouter-only registration when the key is now available.
81
+ */
82
+ private ensureProvidersRegistered;
83
+ }