@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,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning capabilities loader from JSON registry
|
|
3
|
+
* Provides cross-vendor reasoning support detection
|
|
4
|
+
*/
|
|
5
|
+
type ReasoningMode = 'none' | 'effort' | 'max_tokens' | 'both';
|
|
6
|
+
export interface ReasoningVisibilitySupport {
|
|
7
|
+
summary: boolean;
|
|
8
|
+
text: boolean;
|
|
9
|
+
encrypted: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ReasoningCapabilities {
|
|
12
|
+
supportsReasoningDetails: boolean;
|
|
13
|
+
mode: ReasoningMode;
|
|
14
|
+
visibility: ReasoningVisibilitySupport;
|
|
15
|
+
supportedEfforts?: string[];
|
|
16
|
+
vendorId?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load reasoning capabilities from JSON registry based on model ID
|
|
20
|
+
*/
|
|
21
|
+
export declare function getReasoningCapabilitiesFromRegistry(modelId: string): ReasoningCapabilities | null;
|
|
22
|
+
/**
|
|
23
|
+
* Check if model has reasoning parameter in catalog
|
|
24
|
+
*/
|
|
25
|
+
export declare function hasReasoningParamInCatalog(model: any): boolean;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning capabilities loader from JSON registry
|
|
3
|
+
* Provides cross-vendor reasoning support detection
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { getDirname } from '../../utils/esm-compat.js';
|
|
8
|
+
// Get __dirname equivalent for ESM
|
|
9
|
+
const __dirname = getDirname(import.meta.url);
|
|
10
|
+
// Load reasoning support registry from JSON file
|
|
11
|
+
// Using same pattern as openrouter-catalog.ts
|
|
12
|
+
let reasoningSupportRegistry;
|
|
13
|
+
try {
|
|
14
|
+
// __dirname in compiled JS will be dist/adapters/openrouter, so go up to root
|
|
15
|
+
// In TS source, __dirname will be src/adapters/openrouter
|
|
16
|
+
const registryPath = path.join(__dirname, '..', '..', '..', '.metadata', 'reasoning-support.json');
|
|
17
|
+
const registryContent = fs.readFileSync(registryPath, 'utf-8');
|
|
18
|
+
reasoningSupportRegistry = JSON.parse(registryContent);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.warn('[reasoning-capabilities] Failed to load reasoning-support.json, using empty registry:', error);
|
|
22
|
+
reasoningSupportRegistry = { vendors: [] };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load reasoning capabilities from JSON registry based on model ID
|
|
26
|
+
*/
|
|
27
|
+
export function getReasoningCapabilitiesFromRegistry(modelId) {
|
|
28
|
+
const canonicalSlug = modelId.toLowerCase();
|
|
29
|
+
// Iterate through vendors in registry
|
|
30
|
+
for (const vendor of reasoningSupportRegistry.vendors) {
|
|
31
|
+
if (!vendor.supportsReasoningDetails) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Check each model match rule
|
|
35
|
+
for (const rule of vendor.modelMatchRules) {
|
|
36
|
+
let matches = false;
|
|
37
|
+
if (rule.matchType === 'prefix') {
|
|
38
|
+
matches = canonicalSlug.startsWith(rule.pattern.toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
else if (rule.matchType === 'regex') {
|
|
41
|
+
const regex = new RegExp(rule.pattern, 'i');
|
|
42
|
+
matches = regex.test(canonicalSlug);
|
|
43
|
+
}
|
|
44
|
+
else if (rule.matchType === 'exact') {
|
|
45
|
+
matches = canonicalSlug === rule.pattern.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
if (matches) {
|
|
48
|
+
// Determine mode from requestModes
|
|
49
|
+
let mode = 'none';
|
|
50
|
+
if (vendor.requestModes.includes('effort') && vendor.requestModes.includes('max_tokens')) {
|
|
51
|
+
mode = 'both';
|
|
52
|
+
}
|
|
53
|
+
else if (vendor.requestModes.includes('effort')) {
|
|
54
|
+
mode = 'effort';
|
|
55
|
+
}
|
|
56
|
+
else if (vendor.requestModes.includes('max_tokens')) {
|
|
57
|
+
mode = 'max_tokens';
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
supportsReasoningDetails: true,
|
|
61
|
+
mode,
|
|
62
|
+
visibility: vendor.visibilitySupport,
|
|
63
|
+
supportedEfforts: mode === 'effort' || mode === 'both'
|
|
64
|
+
? ['low', 'medium', 'high'] // xhigh normalized to high
|
|
65
|
+
: undefined,
|
|
66
|
+
vendorId: vendor.vendorId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if model has reasoning parameter in catalog
|
|
75
|
+
*/
|
|
76
|
+
export function hasReasoningParamInCatalog(model) {
|
|
77
|
+
return model?.capabilities?.supportedParameters?.includes('reasoning') ||
|
|
78
|
+
model?.capabilities?.supportedParameters?.includes('include_reasoning');
|
|
79
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-discover installed provider packages
|
|
3
|
+
* This is a simplified implementation - in practice, you'd scan node_modules
|
|
4
|
+
* for packages matching the pattern @x12i/llm-provider-*
|
|
5
|
+
*/
|
|
6
|
+
/*
|
|
7
|
+
async function discoverProviders(
|
|
8
|
+
providerConfigs?: Record<string, any>
|
|
9
|
+
): Promise<any[]> {
|
|
10
|
+
const providers: any[] = [];
|
|
11
|
+
|
|
12
|
+
// Common provider package patterns
|
|
13
|
+
const providerPackages = [
|
|
14
|
+
'@x12i/ai-provider-openai',
|
|
15
|
+
'@x12i/ai-provider-grok',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// Also check for @x12i/x-models which may export a provider
|
|
20
|
+
try {
|
|
21
|
+
const xModelsModule = await import('@x12i/x-models').catch(() => null);
|
|
22
|
+
if (xModelsModule) {
|
|
23
|
+
// Look for provider class - check common export patterns
|
|
24
|
+
const XModelsProvider = (xModelsModule as any).XModelsAdapterProvider ||
|
|
25
|
+
(xModelsModule as any).default ||
|
|
26
|
+
(xModelsModule as any).Provider ||
|
|
27
|
+
(xModelsModule as any).LLMProvider;
|
|
28
|
+
if (XModelsProvider && typeof XModelsProvider === 'function') {
|
|
29
|
+
const config = providerConfigs?.['x-models'] || {};
|
|
30
|
+
try {
|
|
31
|
+
const providerInstance = new XModelsProvider(config);
|
|
32
|
+
if (typeof providerInstance.invoke === 'function') {
|
|
33
|
+
providers.push(providerInstance);
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// Provider instantiation failed, skip it
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Package not available, skip it
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const packageName of providerPackages) {
|
|
45
|
+
try {
|
|
46
|
+
// Try to dynamically import the provider
|
|
47
|
+
const providerModule = await import(packageName).catch(() => null);
|
|
48
|
+
if (!providerModule) continue;
|
|
49
|
+
|
|
50
|
+
// Look for default export or named exports
|
|
51
|
+
const exported =
|
|
52
|
+
providerModule.default || providerModule.provider || providerModule[getProviderClassName(packageName)];
|
|
53
|
+
|
|
54
|
+
if (!exported) continue;
|
|
55
|
+
|
|
56
|
+
const providerName = extractProviderName(packageName);
|
|
57
|
+
const config = providerConfigs?.[providerName] || {};
|
|
58
|
+
let providerInstance: any;
|
|
59
|
+
|
|
60
|
+
if (typeof exported === 'function') {
|
|
61
|
+
// Handle class constructor
|
|
62
|
+
try {
|
|
63
|
+
providerInstance = new (exported as any)(config);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// If 'new' fails, try calling as a factory
|
|
66
|
+
try {
|
|
67
|
+
providerInstance = (exported as any)(config);
|
|
68
|
+
} catch (e2) {
|
|
69
|
+
// Both failed
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else if (typeof exported === 'object' && exported !== null) {
|
|
73
|
+
// Handle direct object export (ProviderModule) or factory-like object
|
|
74
|
+
if (typeof (exported as any).initializeClient === 'function') {
|
|
75
|
+
providerInstance = (exported as any).initializeClient(config);
|
|
76
|
+
} else if (exported.name && exported.execute) {
|
|
77
|
+
providerInstance = exported;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (providerInstance) {
|
|
82
|
+
// Check for both invoke (LLMProviderInterface) and aiCall (AIProviderInterface)
|
|
83
|
+
if (typeof providerInstance.invoke === 'function' || typeof providerInstance.aiCall === 'function') {
|
|
84
|
+
providers.push(providerInstance);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
console.warn(`Failed to instantiate provider ${packageName}: No valid constructor or factory found`);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Package not installed or not available, skip it
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return providers;
|
|
96
|
+
}v
|
|
97
|
+
*/
|
|
98
|
+
function getProviderClassName(packageName) {
|
|
99
|
+
const parts = packageName.split('/');
|
|
100
|
+
const name = parts[parts.length - 1];
|
|
101
|
+
// Handle both ai-provider-* and llm-provider-* patterns
|
|
102
|
+
const cleanName = name.replace('ai-provider-', '').replace('llm-provider-', '');
|
|
103
|
+
const className = cleanName
|
|
104
|
+
.split('-')
|
|
105
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
106
|
+
.join('') + 'Provider';
|
|
107
|
+
return className;
|
|
108
|
+
}
|
|
109
|
+
function extractProviderName(packageName) {
|
|
110
|
+
const parts = packageName.split('/');
|
|
111
|
+
const name = parts[parts.length - 1];
|
|
112
|
+
return name.replace('ai-provider-', '').replace('llm-provider-', '');
|
|
113
|
+
}
|
|
114
|
+
export {};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProviderId } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when a requested provider is not registered
|
|
4
|
+
*/
|
|
5
|
+
export declare class ProviderNotFoundError extends Error {
|
|
6
|
+
constructor(providerName: ProviderId | string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when all providers in the fallback chain have failed
|
|
10
|
+
*/
|
|
11
|
+
export declare class FallbackExhaustedError extends Error {
|
|
12
|
+
attempts: Array<{
|
|
13
|
+
provider: ProviderId;
|
|
14
|
+
error: Error;
|
|
15
|
+
}>;
|
|
16
|
+
constructor(attempts: Array<{
|
|
17
|
+
provider: ProviderId;
|
|
18
|
+
error: Error;
|
|
19
|
+
}>);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Error thrown when a provider package is not installed
|
|
23
|
+
*/
|
|
24
|
+
export declare class ProviderNotInstalledError extends Error {
|
|
25
|
+
packageName: string;
|
|
26
|
+
constructor(providerName: ProviderId | string, packageName: string);
|
|
27
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a requested provider is not registered
|
|
3
|
+
*/
|
|
4
|
+
export class ProviderNotFoundError extends Error {
|
|
5
|
+
constructor(providerName) {
|
|
6
|
+
super(`Provider '${providerName}' not registered`);
|
|
7
|
+
this.name = 'ProviderNotFoundError';
|
|
8
|
+
Object.setPrototypeOf(this, ProviderNotFoundError.prototype);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Error thrown when all providers in the fallback chain have failed
|
|
13
|
+
*/
|
|
14
|
+
export class FallbackExhaustedError extends Error {
|
|
15
|
+
constructor(attempts) {
|
|
16
|
+
super('All providers in fallback chain failed');
|
|
17
|
+
this.attempts = attempts;
|
|
18
|
+
this.name = 'FallbackExhaustedError';
|
|
19
|
+
Object.setPrototypeOf(this, FallbackExhaustedError.prototype);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Error thrown when a provider package is not installed
|
|
24
|
+
*/
|
|
25
|
+
export class ProviderNotInstalledError extends Error {
|
|
26
|
+
constructor(providerName, packageName) {
|
|
27
|
+
super(`Provider '${providerName}' is not installed. ` +
|
|
28
|
+
`Install it with: npm install ${packageName} or yarn add ${packageName}`);
|
|
29
|
+
this.packageName = packageName;
|
|
30
|
+
this.name = 'ProviderNotInstalledError';
|
|
31
|
+
Object.setPrototypeOf(this, ProviderNotInstalledError.prototype);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LLMProviderRouter } from './router/RouterWrapper.js';
|
|
2
|
+
import type { RouterConfig } from './router/RouterTypes.js';
|
|
3
|
+
export interface CreateRouterConfig extends RouterConfig {
|
|
4
|
+
providerConfigs?: Record<string, any>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Factory function to create a router with automatic provider discovery and configuration
|
|
8
|
+
* ERC-compliant: Supports zero-config initialization from environment variables
|
|
9
|
+
*/
|
|
10
|
+
export declare function createRouter(config?: CreateRouterConfig): Promise<LLMProviderRouter>;
|
|
11
|
+
/**
|
|
12
|
+
* Create router from configuration file (legacy support)
|
|
13
|
+
* Note: For ERC compliance, prefer createRouter() with environment variables
|
|
14
|
+
*/
|
|
15
|
+
export declare function createRouterFromConfig(configPath: string): Promise<LLMProviderRouter>;
|
package/dist/factory.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { LLMProviderRouter } from './router/RouterWrapper.js';
|
|
2
|
+
import dns from 'node:dns';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
// Fix IPv6-first DNS resolution issue on Windows (forces IPv4-first to avoid connect timeouts)
|
|
5
|
+
// This prevents undici from trying IPv6 first on networks that silently drop IPv6 traffic
|
|
6
|
+
// Must be set before any network calls are made
|
|
7
|
+
dns.setDefaultResultOrder('ipv4first');
|
|
8
|
+
// Set undici global dispatcher connectTimeout to prevent hard 10s connect timeout
|
|
9
|
+
// This must be done before any provider initializes to ensure all fetch calls use the correct timeout
|
|
10
|
+
// Dynamic import for optional dependency (undici might not be available)
|
|
11
|
+
(async () => {
|
|
12
|
+
try {
|
|
13
|
+
const { Agent, setGlobalDispatcher } = await import('undici');
|
|
14
|
+
// Set a reasonable default connect timeout (60s) - providers can override via their config
|
|
15
|
+
// This prevents undici's hard-coded 10s connect timeout from causing premature failures
|
|
16
|
+
const defaultConnectTimeout = 60000; // 60 seconds
|
|
17
|
+
setGlobalDispatcher(new Agent({
|
|
18
|
+
connectTimeout: defaultConnectTimeout,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
// undici not available or already set - continue without it
|
|
23
|
+
// This is a best-effort fix; providers should also set their own dispatchers
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
// Load environment variables from .env file if available
|
|
27
|
+
// This ensures nx-config2 can resolve ENV. placeholders
|
|
28
|
+
// dotenv.config() is idempotent and won't overwrite existing process.env values
|
|
29
|
+
dotenv.config();
|
|
30
|
+
/**
|
|
31
|
+
* Resolve ENV. placeholders to actual environment variable values
|
|
32
|
+
* Replaces nx-config2 functionality to avoid ESM/CommonJS compatibility issues
|
|
33
|
+
*/
|
|
34
|
+
function resolveEnvPlaceholders(config) {
|
|
35
|
+
if (typeof config === 'string') {
|
|
36
|
+
// Handle ENV.VAR_NAME||default pattern
|
|
37
|
+
if (config.startsWith('ENV.')) {
|
|
38
|
+
const match = config.match(/^ENV\.([^|]+)(?:\|\|(.+))?$/);
|
|
39
|
+
if (match) {
|
|
40
|
+
const envVar = match[1];
|
|
41
|
+
const defaultValue = match[2] !== undefined ? match[2] : undefined;
|
|
42
|
+
const value = process.env[envVar];
|
|
43
|
+
return value !== undefined ? value : (defaultValue !== undefined ? defaultValue : config);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(config)) {
|
|
49
|
+
return config.map(item => resolveEnvPlaceholders(item));
|
|
50
|
+
}
|
|
51
|
+
if (config && typeof config === 'object') {
|
|
52
|
+
const resolved = {};
|
|
53
|
+
for (const [key, value] of Object.entries(config)) {
|
|
54
|
+
resolved[key] = resolveEnvPlaceholders(value);
|
|
55
|
+
}
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Legacy provider inference function (fallback when catalog is unavailable)
|
|
62
|
+
*/
|
|
63
|
+
function inferProviderFromModelLegacy(model) {
|
|
64
|
+
if (model.startsWith('gpt-') || model.startsWith('o1-') || model.startsWith('openai/')) {
|
|
65
|
+
return 'openai';
|
|
66
|
+
}
|
|
67
|
+
else if (model.startsWith('claude-') || model.startsWith('anthropic/')) {
|
|
68
|
+
return 'anthropic';
|
|
69
|
+
}
|
|
70
|
+
else if (model.startsWith('grok-') || model.startsWith('xai/')) {
|
|
71
|
+
return 'grok';
|
|
72
|
+
}
|
|
73
|
+
else if (model.startsWith('gemini-') || model.startsWith('google/')) {
|
|
74
|
+
return 'google';
|
|
75
|
+
}
|
|
76
|
+
else if (model.startsWith('llama-') || model.startsWith('meta-llama/')) {
|
|
77
|
+
return 'meta';
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Factory function to create a router with automatic provider discovery and configuration
|
|
83
|
+
* ERC-compliant: Supports zero-config initialization from environment variables
|
|
84
|
+
*/
|
|
85
|
+
export async function createRouter(config) {
|
|
86
|
+
// ERC Configuration: Define router and provider requirements
|
|
87
|
+
// Use our own resolver instead of nx-config2 to avoid ESM/CommonJS compatibility issues
|
|
88
|
+
const ercConfig = {
|
|
89
|
+
// Router-specific configuration
|
|
90
|
+
router: {
|
|
91
|
+
logLevel: 'ENV.AI_PROVIDER_ROUTER_LOG_LEVEL||info',
|
|
92
|
+
verbose: 'ENV.AI_PROVIDER_ROUTER_VERBOSE||false',
|
|
93
|
+
timeoutMs: 'ENV.AI_PROVIDER_ROUTER_TIMEOUT_MS||60000',
|
|
94
|
+
},
|
|
95
|
+
// Provider SDK client configurations (auto-detected from environment)
|
|
96
|
+
providers: {
|
|
97
|
+
openai: {
|
|
98
|
+
apiKey: 'ENV.OPENAI_API_KEY',
|
|
99
|
+
baseURL: 'ENV.OPENAI_API_BASE',
|
|
100
|
+
organization: 'ENV.OPENAI_ORGANIZATION',
|
|
101
|
+
},
|
|
102
|
+
xai: {
|
|
103
|
+
apiKey: 'ENV.GROK_API_KEY',
|
|
104
|
+
baseURL: 'ENV.XAI_API_BASE',
|
|
105
|
+
},
|
|
106
|
+
groq: {
|
|
107
|
+
apiKey: 'ENV.GROQ_API_KEY',
|
|
108
|
+
},
|
|
109
|
+
anthropic: {
|
|
110
|
+
apiKey: 'ENV.ANTHROPIC_API_KEY',
|
|
111
|
+
baseURL: 'ENV.ANTHROPIC_API_BASE',
|
|
112
|
+
},
|
|
113
|
+
google: {
|
|
114
|
+
apiKey: 'ENV.GOOGLE_API_KEY',
|
|
115
|
+
},
|
|
116
|
+
openrouter: {
|
|
117
|
+
apiKey: 'ENV.OPEN_ROUTER_KEY',
|
|
118
|
+
httpReferer: 'ENV.OPEN_ROUTER_HTTP_REFERER',
|
|
119
|
+
xTitle: 'ENV.OPEN_ROUTER_X_TITLE',
|
|
120
|
+
useOpenRouter: 'ENV.USE_OPENROUTER',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
// Resolve ENV. placeholders
|
|
125
|
+
const ercResult = {
|
|
126
|
+
config: resolveEnvPlaceholders(ercConfig)
|
|
127
|
+
};
|
|
128
|
+
// Use explicit config if provided (Advanced Mode), otherwise use ERC auto-discovered config (Zero-Config Mode)
|
|
129
|
+
const finalConfig = Object.keys(config || {}).length > 0 ? config : {
|
|
130
|
+
logLevel: ercResult.config.router.logLevel,
|
|
131
|
+
verbose: ercResult.config.router.verbose === 'true' || ercResult.config.router.verbose === true,
|
|
132
|
+
timeoutMs: ercResult.config.router.timeoutMs ? parseInt(String(ercResult.config.router.timeoutMs), 10) : undefined,
|
|
133
|
+
defaultTimeoutMs: ercResult.config.router.timeoutMs ? parseInt(String(ercResult.config.router.timeoutMs), 10) : undefined,
|
|
134
|
+
};
|
|
135
|
+
const router = new LLMProviderRouter(finalConfig);
|
|
136
|
+
// Configure providers from ERC auto-detected config OR explicit config
|
|
137
|
+
const providerConfigs = config?.providerConfigs || ercResult.config.providers;
|
|
138
|
+
// Check if OpenRouter mode should be enabled
|
|
139
|
+
// Default: true if OPEN_ROUTER_KEY is present, else false
|
|
140
|
+
// Can be explicitly disabled with USE_OPENROUTER=false
|
|
141
|
+
const openRouterKey = providerConfigs.openrouter?.apiKey;
|
|
142
|
+
const useOpenRouterEnv = providerConfigs.openrouter?.useOpenRouter;
|
|
143
|
+
// Check if API key is valid (not placeholder, not empty)
|
|
144
|
+
const hasValidKey = openRouterKey &&
|
|
145
|
+
openRouterKey !== 'ENV.OPEN_ROUTER_KEY' &&
|
|
146
|
+
typeof openRouterKey === 'string' &&
|
|
147
|
+
openRouterKey.trim() !== '';
|
|
148
|
+
// Determine if OpenRouter should be enabled
|
|
149
|
+
// Default to true if key is present, unless explicitly disabled
|
|
150
|
+
let useOpenRouter = false;
|
|
151
|
+
if (hasValidKey) {
|
|
152
|
+
if (useOpenRouterEnv === undefined || useOpenRouterEnv === '' || useOpenRouterEnv === 'true' || useOpenRouterEnv === true) {
|
|
153
|
+
useOpenRouter = true;
|
|
154
|
+
}
|
|
155
|
+
else if (useOpenRouterEnv === 'false' || useOpenRouterEnv === false) {
|
|
156
|
+
useOpenRouter = false;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Default to true if key is present
|
|
160
|
+
useOpenRouter = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// NOTE: We don't need to manually register providers here anymore if they are
|
|
164
|
+
// in environment variables, as LLMProviderRouter will auto-register them
|
|
165
|
+
// on first invoke/stream/createBatch call.
|
|
166
|
+
// We only register them here if they were provided in the explicit config.
|
|
167
|
+
if (config?.providerConfigs) {
|
|
168
|
+
// Manual registration from explicit config
|
|
169
|
+
for (const [name, pConfig] of Object.entries(config.providerConfigs)) {
|
|
170
|
+
if (pConfig && pConfig.apiKey && !pConfig.apiKey.startsWith('ENV.')) {
|
|
171
|
+
router.configureProvider(name, pConfig);
|
|
172
|
+
try {
|
|
173
|
+
if (name === 'openai') {
|
|
174
|
+
const mod = await import('@x12i/ai-provider-openai');
|
|
175
|
+
router.registerProvider(mod, 'initializeClient');
|
|
176
|
+
}
|
|
177
|
+
else if (name === 'grok' || name === 'xai') {
|
|
178
|
+
const mod = await import('@x12i/ai-provider-grok');
|
|
179
|
+
router.registerProvider(mod);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
// Skip if provider package not installed
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return router;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Create router from configuration file (legacy support)
|
|
192
|
+
* Note: For ERC compliance, prefer createRouter() with environment variables
|
|
193
|
+
*/
|
|
194
|
+
export async function createRouterFromConfig(configPath) {
|
|
195
|
+
// Load config file
|
|
196
|
+
const fs = await import('fs/promises');
|
|
197
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
198
|
+
const fileConfig = JSON.parse(configContent);
|
|
199
|
+
// Convert file config to router config format
|
|
200
|
+
const routerConfig = {
|
|
201
|
+
providerConfigs: fileConfig.providers,
|
|
202
|
+
logLevel: fileConfig.logLevel,
|
|
203
|
+
verbose: fileConfig.verbose,
|
|
204
|
+
};
|
|
205
|
+
return createRouter(routerConfig);
|
|
206
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LLMProviderRouter } from './router/RouterWrapper.js';
|
|
2
|
+
import type { AIResponse } from './router/RouterTypes.js';
|
|
3
|
+
export interface EnhancedLLMResponse extends AIResponse {
|
|
4
|
+
content?: {
|
|
5
|
+
providerMetadata?: {
|
|
6
|
+
provider?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
};
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
};
|
|
12
|
+
metadata?: any;
|
|
13
|
+
}
|
|
14
|
+
export declare class AIGateway {
|
|
15
|
+
private router;
|
|
16
|
+
constructor(router?: LLMProviderRouter);
|
|
17
|
+
invoke(request: any): Promise<EnhancedLLMResponse>;
|
|
18
|
+
private invokeInternal;
|
|
19
|
+
private validateRequest;
|
|
20
|
+
private extractOutboundPrompt;
|
|
21
|
+
private looksLikeConfigJSON;
|
|
22
|
+
}
|