@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
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { LLMProviderRouter } from './router/RouterWrapper.js';
|
|
2
|
+
export class AIGateway {
|
|
3
|
+
constructor(router) {
|
|
4
|
+
this.router = router || new LLMProviderRouter();
|
|
5
|
+
}
|
|
6
|
+
async invoke(request) {
|
|
7
|
+
// Accept router-wrapper requests too (provider/mode/request), and unwrap them
|
|
8
|
+
// so gateway doesn't treat them as an instruction object and run the instruction pipeline.
|
|
9
|
+
if (request && typeof request === 'object' && request.request && request.provider && request.mode) {
|
|
10
|
+
const inner = request.request;
|
|
11
|
+
request = {
|
|
12
|
+
...inner,
|
|
13
|
+
// preserve provider/mode semantics in the gateway config
|
|
14
|
+
config: {
|
|
15
|
+
...(inner.config || {}),
|
|
16
|
+
provider: (inner.config && inner.config.provider) || request.provider,
|
|
17
|
+
},
|
|
18
|
+
// optional: keep mode for downstream if you use it
|
|
19
|
+
mode: request.mode,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
this.validateRequest(request);
|
|
23
|
+
const requestedProvider = request?.config?.provider;
|
|
24
|
+
const requestedModel = request?.config?.model;
|
|
25
|
+
const response = await this.invokeInternal(request);
|
|
26
|
+
// Optional: Diagnostic warning for JSON-looking prompts (doesn't block, just warns)
|
|
27
|
+
const outboundPrompt = this.extractOutboundPrompt(request);
|
|
28
|
+
if (outboundPrompt && this.looksLikeConfigJSON(outboundPrompt)) {
|
|
29
|
+
console.warn(`[WARNING] Outbound prompt appears to be JSON config data instead of user instructions. ` +
|
|
30
|
+
`This may indicate config data reached the AI model. Prompt preview: ${outboundPrompt.substring(0, 100)}...`);
|
|
31
|
+
}
|
|
32
|
+
// Enforce provider/model pinning when explicitly requested.
|
|
33
|
+
// Default behavior: warn only. Enable strict pinning via config flag.
|
|
34
|
+
const enforcePinning = request?.config?.enforceProviderModel === true;
|
|
35
|
+
const actualProvider = response?.content?.providerMetadata?.provider ||
|
|
36
|
+
response?.provider ||
|
|
37
|
+
response?.metadata?.provider;
|
|
38
|
+
const actualModel = response?.content?.providerMetadata?.model ||
|
|
39
|
+
response?.model ||
|
|
40
|
+
response?.metadata?.model;
|
|
41
|
+
if (enforcePinning && (requestedProvider || requestedModel)) {
|
|
42
|
+
if (requestedProvider && actualProvider && requestedProvider !== actualProvider) {
|
|
43
|
+
throw new Error(`Provider mismatch: requested '${requestedProvider}' but executed '${actualProvider}'. Refusing silent provider switching.`);
|
|
44
|
+
}
|
|
45
|
+
if (requestedModel && actualModel && requestedModel !== actualModel) {
|
|
46
|
+
throw new Error(`Model mismatch: requested '${requestedModel}' but executed '${actualModel}'. Refusing silent model fallback.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
async invokeInternal(request) {
|
|
52
|
+
// Convert the request to router format and invoke
|
|
53
|
+
const routerRequest = {
|
|
54
|
+
provider: request.config?.provider || 'openai',
|
|
55
|
+
mode: request.mode || 'sync',
|
|
56
|
+
request: {
|
|
57
|
+
instructions: request.instructions || '',
|
|
58
|
+
inputData: request.inputData || '',
|
|
59
|
+
config: request.config || {},
|
|
60
|
+
},
|
|
61
|
+
exec: request.exec || {},
|
|
62
|
+
};
|
|
63
|
+
return await this.router.invoke(routerRequest);
|
|
64
|
+
}
|
|
65
|
+
validateRequest(request) {
|
|
66
|
+
if (!request) {
|
|
67
|
+
throw new Error('Request is required');
|
|
68
|
+
}
|
|
69
|
+
// 🚨 CRITICAL VALIDATION: Prevent config data from being sent as instructions
|
|
70
|
+
// This happens when adapters fall back to JSON.stringify(request) and send config to AI model
|
|
71
|
+
// Validate instruction content types
|
|
72
|
+
if (request.instructions !== undefined && typeof request.instructions !== 'string') {
|
|
73
|
+
throw new Error(`CRITICAL: instructions must be a string, got ${typeof request.instructions}. ` +
|
|
74
|
+
'Non-string instructions can cause adapters to stringify the entire request object.');
|
|
75
|
+
}
|
|
76
|
+
if (request.inputData !== undefined) {
|
|
77
|
+
if (typeof request.inputData !== 'string' && typeof request.inputData !== 'object') {
|
|
78
|
+
throw new Error(`CRITICAL: inputData must be a string or object, got ${typeof request.inputData}. ` +
|
|
79
|
+
'Invalid inputData types can cause adapters to stringify the entire request object.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Validate messages structure
|
|
83
|
+
if (request.messages !== undefined) {
|
|
84
|
+
if (!Array.isArray(request.messages)) {
|
|
85
|
+
throw new Error(`CRITICAL: messages must be an array, got ${typeof request.messages}. ` +
|
|
86
|
+
'Invalid messages structure can cause adapters to stringify the entire request object.');
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < request.messages.length; i++) {
|
|
89
|
+
const msg = request.messages[i];
|
|
90
|
+
if (!msg || typeof msg !== 'object') {
|
|
91
|
+
throw new Error(`CRITICAL: messages[${i}] must be an object, got ${typeof msg}. ` +
|
|
92
|
+
'Invalid message structure can cause adapters to stringify the entire request object.');
|
|
93
|
+
}
|
|
94
|
+
if (msg.content !== undefined && typeof msg.content !== 'string') {
|
|
95
|
+
throw new Error(`CRITICAL: messages[${i}].content must be a string, got ${typeof msg.content}. ` +
|
|
96
|
+
'Non-string message content can cause adapters to stringify the entire request object.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Require at least one valid instruction source
|
|
101
|
+
const hasValidInstructions = request.instructions && typeof request.instructions === 'string' && request.instructions.trim().length > 0;
|
|
102
|
+
const hasValidInputData = request.inputData && ((typeof request.inputData === 'string' && request.inputData.trim().length > 0) ||
|
|
103
|
+
(typeof request.inputData === 'object' && Object.keys(request.inputData).length > 0));
|
|
104
|
+
const hasValidUserMessage = request.messages && Array.isArray(request.messages) &&
|
|
105
|
+
request.messages.some((msg) => msg.role === 'user' && msg.content && typeof msg.content === 'string' && msg.content.trim().length > 0);
|
|
106
|
+
if (!hasValidInstructions && !hasValidInputData && !hasValidUserMessage) {
|
|
107
|
+
throw new Error('CRITICAL: Request must have at least one valid instruction source. ' +
|
|
108
|
+
'Provide either: non-empty string instructions, valid inputData, or at least one user message with string content. ' +
|
|
109
|
+
'Missing/empty instruction content causes adapters to fall back to JSON.stringify(request), ' +
|
|
110
|
+
'which sends configuration data to the AI model instead of proper instructions.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
extractOutboundPrompt(request) {
|
|
114
|
+
// Extract what will actually be sent to the AI model as the prompt/instructions
|
|
115
|
+
// Check inputData (most common)
|
|
116
|
+
if (request.inputData && typeof request.inputData === 'string' && request.inputData.trim()) {
|
|
117
|
+
return request.inputData.trim();
|
|
118
|
+
}
|
|
119
|
+
// Check instructions
|
|
120
|
+
if (request.instructions && typeof request.instructions === 'string' && request.instructions.trim()) {
|
|
121
|
+
return request.instructions.trim();
|
|
122
|
+
}
|
|
123
|
+
// Check messages
|
|
124
|
+
if (request.messages && Array.isArray(request.messages)) {
|
|
125
|
+
for (const msg of request.messages) {
|
|
126
|
+
if (msg.role === 'user' && msg.content && typeof msg.content === 'string' && msg.content.trim()) {
|
|
127
|
+
return msg.content.trim();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
looksLikeConfigJSON(text) {
|
|
134
|
+
if (!text || typeof text !== 'string')
|
|
135
|
+
return false;
|
|
136
|
+
const trimmed = text.trim();
|
|
137
|
+
if (!trimmed.startsWith('{'))
|
|
138
|
+
return false;
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(trimmed);
|
|
141
|
+
if (typeof parsed !== 'object' || !parsed)
|
|
142
|
+
return false;
|
|
143
|
+
// Look for config-like patterns
|
|
144
|
+
const keys = Object.keys(parsed);
|
|
145
|
+
const configIndicators = ['config', 'provider', 'model', 'apiKey', 'maxTokens', 'temperature'];
|
|
146
|
+
// If it has multiple config keys, it's likely config JSON
|
|
147
|
+
const configKeyCount = keys.filter(key => configIndicators.includes(key)).length;
|
|
148
|
+
return configKeyCount >= 2;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false; // Not valid JSON
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { LLMProviderRouter } from './router.js';
|
|
2
|
+
export type { RouterConfig, HealthCheckResult, ProviderId, AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem, } from './router.js';
|
|
3
|
+
export { createRouter, createRouterFromConfig } from './factory.js';
|
|
4
|
+
export type { CreateRouterConfig } from './factory.js';
|
|
5
|
+
export { ProviderNotFoundError, FallbackExhaustedError, ProviderNotInstalledError } from './errors.js';
|
|
6
|
+
export type { RequestInterceptor, ResponseInterceptor } from './interceptors.js';
|
|
7
|
+
export type { UsageTracker, AdapterLoader, ProviderInit } from './types.js';
|
|
8
|
+
export { Logger, getLogger, createLogger } from './logger.js';
|
|
9
|
+
export type { LogLevel, LoggerConfig } from './logger.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Fix IPv6-first DNS resolution issue on Windows (forces IPv4-first to avoid connect timeouts)
|
|
2
|
+
// This prevents undici from trying IPv6 first on networks that silently drop IPv6 traffic
|
|
3
|
+
// Must be set before any network calls are made
|
|
4
|
+
import dns from 'node:dns';
|
|
5
|
+
dns.setDefaultResultOrder('ipv4first');
|
|
6
|
+
// Main exports
|
|
7
|
+
export { LLMProviderRouter } from './router.js';
|
|
8
|
+
// Provider/model enums and helpers
|
|
9
|
+
/*
|
|
10
|
+
export {
|
|
11
|
+
AIProvider,
|
|
12
|
+
NxAiProvider,
|
|
13
|
+
OpenAIModel,
|
|
14
|
+
GroqModel,
|
|
15
|
+
AnthropicModel,
|
|
16
|
+
GoogleModel,
|
|
17
|
+
GoogleEmbeddingModel,
|
|
18
|
+
MistralModel,
|
|
19
|
+
CohereModel,
|
|
20
|
+
CohereEmbeddingModel,
|
|
21
|
+
MetaModel,
|
|
22
|
+
DeepSeekModel,
|
|
23
|
+
XAIModel,
|
|
24
|
+
AmazonModel,
|
|
25
|
+
QwenModel,
|
|
26
|
+
PerplexityModel,
|
|
27
|
+
VoyageModel,
|
|
28
|
+
TogetherModel,
|
|
29
|
+
FireworksModel,
|
|
30
|
+
OllamaModel,
|
|
31
|
+
listModelsByProvider,
|
|
32
|
+
isValidModelForProvider,
|
|
33
|
+
mapProviderNameToEnum,
|
|
34
|
+
} from './models.js';
|
|
35
|
+
*/
|
|
36
|
+
//export type { ProviderModelRef } from './models.js';
|
|
37
|
+
// Factory functions
|
|
38
|
+
export { createRouter, createRouterFromConfig } from './factory.js';
|
|
39
|
+
// Error classes
|
|
40
|
+
export { ProviderNotFoundError, FallbackExhaustedError, ProviderNotInstalledError } from './errors.js';
|
|
41
|
+
// Logger
|
|
42
|
+
export { Logger, getLogger, createLogger } from './logger.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AIRouterRequest, AIResponse } from './router/RouterTypes.js';
|
|
2
|
+
import type { ProviderId } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Interceptor function that can modify requests before they are sent
|
|
5
|
+
*/
|
|
6
|
+
export type RequestInterceptor = (request: AIRouterRequest, provider?: ProviderId) => AIRouterRequest | Promise<AIRouterRequest>;
|
|
7
|
+
/**
|
|
8
|
+
* Interceptor function that can modify responses after they are received
|
|
9
|
+
*/
|
|
10
|
+
export type ResponseInterceptor = (response: AIResponse, provider: ProviderId) => AIResponse | Promise<AIResponse>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging utility for AI Provider Router
|
|
3
|
+
* Provides structured logging with proper log levels and verbose mode support
|
|
4
|
+
*/
|
|
5
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose';
|
|
6
|
+
export interface LoggerConfig {
|
|
7
|
+
verbose?: boolean;
|
|
8
|
+
level?: LogLevel;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Logger class that wraps logxer with proper log levels
|
|
12
|
+
*/
|
|
13
|
+
export declare class Logger {
|
|
14
|
+
verbose: boolean;
|
|
15
|
+
level: LogLevel;
|
|
16
|
+
private gateway;
|
|
17
|
+
constructor(config?: LoggerConfig);
|
|
18
|
+
/**
|
|
19
|
+
* Update logger configuration
|
|
20
|
+
*/
|
|
21
|
+
setConfig(config: LoggerConfig): void;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a log level should be logged
|
|
24
|
+
*/
|
|
25
|
+
private shouldLog;
|
|
26
|
+
/**
|
|
27
|
+
* Log error messages
|
|
28
|
+
*/
|
|
29
|
+
error(message: string, data?: any): void;
|
|
30
|
+
/**
|
|
31
|
+
* Log warning messages
|
|
32
|
+
*/
|
|
33
|
+
warn(message: string, data?: any): void;
|
|
34
|
+
/**
|
|
35
|
+
* Log informational messages
|
|
36
|
+
*/
|
|
37
|
+
info(message: string, data?: any): void;
|
|
38
|
+
/**
|
|
39
|
+
* Log debug messages
|
|
40
|
+
*/
|
|
41
|
+
debug(message: string, data?: any): void;
|
|
42
|
+
/**
|
|
43
|
+
* Log verbose messages (only when verbose mode is enabled)
|
|
44
|
+
*/
|
|
45
|
+
logVerbose(message: string, data?: any): void;
|
|
46
|
+
/**
|
|
47
|
+
* Log AI request (unfiltered, only in verbose mode)
|
|
48
|
+
*/
|
|
49
|
+
logAIRequest(provider: string, request: any, metadata?: any): void;
|
|
50
|
+
/**
|
|
51
|
+
* Log AI response (unfiltered, only in verbose mode)
|
|
52
|
+
*/
|
|
53
|
+
logAIResponse(provider: string, response: any, metadata?: any): void;
|
|
54
|
+
/**
|
|
55
|
+
* Log AI request/response pair (unfiltered, only in verbose mode)
|
|
56
|
+
*/
|
|
57
|
+
logAIIteraction(provider: string, request: any, response: any, duration?: number, metadata?: any): void;
|
|
58
|
+
/**
|
|
59
|
+
* Sanitize data for logging (remove sensitive info, handle circular refs)
|
|
60
|
+
*/
|
|
61
|
+
sanitizeForLogging(data: any): any;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get or create the default logger instance
|
|
65
|
+
*/
|
|
66
|
+
export declare function getLogger(config?: LoggerConfig): Logger;
|
|
67
|
+
/**
|
|
68
|
+
* Create a new logger instance
|
|
69
|
+
*/
|
|
70
|
+
export declare function createLogger(config?: LoggerConfig): Logger;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging utility for AI Provider Router
|
|
3
|
+
* Provides structured logging with proper log levels and verbose mode support
|
|
4
|
+
*/
|
|
5
|
+
import logxer from '@x12i/logxer';
|
|
6
|
+
const logs = logxer || console;
|
|
7
|
+
/**
|
|
8
|
+
* Logger class that wraps logxer with proper log levels
|
|
9
|
+
*/
|
|
10
|
+
export class Logger {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.verbose = config.verbose || false;
|
|
13
|
+
this.level = config.level || 'info';
|
|
14
|
+
this.gateway = logs || console;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Update logger configuration
|
|
18
|
+
*/
|
|
19
|
+
setConfig(config) {
|
|
20
|
+
if (config.verbose !== undefined) {
|
|
21
|
+
this.verbose = config.verbose;
|
|
22
|
+
}
|
|
23
|
+
if (config.level !== undefined) {
|
|
24
|
+
this.level = config.level;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if a log level should be logged
|
|
29
|
+
*/
|
|
30
|
+
shouldLog(level) {
|
|
31
|
+
const levels = ['error', 'warn', 'info', 'debug', 'verbose'];
|
|
32
|
+
const currentIndex = levels.indexOf(this.level);
|
|
33
|
+
const messageIndex = levels.indexOf(level);
|
|
34
|
+
return messageIndex <= currentIndex;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Log error messages
|
|
38
|
+
*/
|
|
39
|
+
error(message, data) {
|
|
40
|
+
if (!this.shouldLog('error'))
|
|
41
|
+
return;
|
|
42
|
+
if (this.gateway?.error) {
|
|
43
|
+
this.gateway.error(message, data || {});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.error(`[ERROR] ${message}`, data || '');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Log warning messages
|
|
51
|
+
*/
|
|
52
|
+
warn(message, data) {
|
|
53
|
+
if (!this.shouldLog('warn'))
|
|
54
|
+
return;
|
|
55
|
+
if (this.gateway?.warn) {
|
|
56
|
+
this.gateway.warn(message, data || {});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.warn(`[WARN] ${message}`, data || '');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Log informational messages
|
|
64
|
+
*/
|
|
65
|
+
info(message, data) {
|
|
66
|
+
if (!this.shouldLog('info'))
|
|
67
|
+
return;
|
|
68
|
+
if (this.gateway?.info) {
|
|
69
|
+
this.gateway.info(message, data || {});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(`[INFO] ${message}`, data || '');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Log debug messages
|
|
77
|
+
*/
|
|
78
|
+
debug(message, data) {
|
|
79
|
+
if (!this.shouldLog('debug'))
|
|
80
|
+
return;
|
|
81
|
+
if (this.gateway?.debug) {
|
|
82
|
+
this.gateway.debug(message, data || {});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.debug(`[DEBUG] ${message}`, data || '');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Log verbose messages (only when verbose mode is enabled)
|
|
90
|
+
*/
|
|
91
|
+
logVerbose(message, data) {
|
|
92
|
+
if (!this.verbose || !this.shouldLog('verbose'))
|
|
93
|
+
return;
|
|
94
|
+
if (this.gateway?.verbose) {
|
|
95
|
+
this.gateway.verbose(message, data || {});
|
|
96
|
+
}
|
|
97
|
+
else if (this.gateway?.trace) {
|
|
98
|
+
this.gateway.trace(message, data || {});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(`[VERBOSE] ${message}`, data || '');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Log AI request (unfiltered, only in verbose mode)
|
|
106
|
+
*/
|
|
107
|
+
logAIRequest(provider, request, metadata) {
|
|
108
|
+
if (!this.verbose)
|
|
109
|
+
return;
|
|
110
|
+
this.logVerbose('AI Request Sent', {
|
|
111
|
+
provider,
|
|
112
|
+
request: this.sanitizeForLogging(request),
|
|
113
|
+
metadata: metadata || {},
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Log AI response (unfiltered, only in verbose mode)
|
|
119
|
+
*/
|
|
120
|
+
logAIResponse(provider, response, metadata) {
|
|
121
|
+
if (!this.verbose)
|
|
122
|
+
return;
|
|
123
|
+
this.logVerbose('AI Response Received', {
|
|
124
|
+
provider,
|
|
125
|
+
response: this.sanitizeForLogging(response),
|
|
126
|
+
metadata: metadata || {},
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Log AI request/response pair (unfiltered, only in verbose mode)
|
|
132
|
+
*/
|
|
133
|
+
logAIIteraction(provider, request, response, duration, metadata) {
|
|
134
|
+
if (!this.verbose)
|
|
135
|
+
return;
|
|
136
|
+
this.logVerbose('AI Interaction Complete', {
|
|
137
|
+
provider,
|
|
138
|
+
request: this.sanitizeForLogging(request),
|
|
139
|
+
response: this.sanitizeForLogging(response),
|
|
140
|
+
duration: duration ? `${duration}ms` : undefined,
|
|
141
|
+
metadata: metadata || {},
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sanitize data for logging (remove sensitive info, handle circular refs)
|
|
147
|
+
*/
|
|
148
|
+
sanitizeForLogging(data) {
|
|
149
|
+
if (data === null || data === undefined) {
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
// Handle circular references and large objects
|
|
153
|
+
const seen = new WeakSet();
|
|
154
|
+
const sanitize = (obj, depth = 0) => {
|
|
155
|
+
// Prevent infinite recursion
|
|
156
|
+
if (depth > 10) {
|
|
157
|
+
return '[Max Depth Reached]';
|
|
158
|
+
}
|
|
159
|
+
// Handle primitives
|
|
160
|
+
if (obj === null || obj === undefined) {
|
|
161
|
+
return obj;
|
|
162
|
+
}
|
|
163
|
+
if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
|
|
164
|
+
return obj;
|
|
165
|
+
}
|
|
166
|
+
// Handle circular references
|
|
167
|
+
if (typeof obj === 'object') {
|
|
168
|
+
if (seen.has(obj)) {
|
|
169
|
+
return '[Circular Reference]';
|
|
170
|
+
}
|
|
171
|
+
seen.add(obj);
|
|
172
|
+
// Handle arrays
|
|
173
|
+
if (Array.isArray(obj)) {
|
|
174
|
+
return obj.map((item) => sanitize(item, depth + 1));
|
|
175
|
+
}
|
|
176
|
+
// Handle Buffer (common in file uploads)
|
|
177
|
+
if (Buffer.isBuffer(obj)) {
|
|
178
|
+
return `[Buffer: ${obj.length} bytes]`;
|
|
179
|
+
}
|
|
180
|
+
// Handle objects
|
|
181
|
+
const result = {};
|
|
182
|
+
for (const key in obj) {
|
|
183
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
184
|
+
// Skip sensitive keys (can be extended)
|
|
185
|
+
if (key.toLowerCase().includes('password') ||
|
|
186
|
+
key.toLowerCase().includes('secret') ||
|
|
187
|
+
key.toLowerCase().includes('apikey') ||
|
|
188
|
+
key.toLowerCase().includes('token') && key !== 'token') {
|
|
189
|
+
result[key] = '[REDACTED]';
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
result[key] = sanitize(obj[key], depth + 1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
return String(obj);
|
|
199
|
+
};
|
|
200
|
+
return sanitize(data);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Default logger instance
|
|
204
|
+
let defaultLogger = null;
|
|
205
|
+
/**
|
|
206
|
+
* Get or create the default logger instance
|
|
207
|
+
*/
|
|
208
|
+
export function getLogger(config) {
|
|
209
|
+
if (!defaultLogger) {
|
|
210
|
+
defaultLogger = new Logger(config);
|
|
211
|
+
}
|
|
212
|
+
else if (config) {
|
|
213
|
+
defaultLogger.setConfig(config);
|
|
214
|
+
}
|
|
215
|
+
return defaultLogger;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Create a new logger instance
|
|
219
|
+
*/
|
|
220
|
+
export function createLogger(config = {}) {
|
|
221
|
+
return new Logger(config);
|
|
222
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export interface OpenRouterProvider {
|
|
2
|
+
slug: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface OpenRouterModelVendor {
|
|
6
|
+
author: string;
|
|
7
|
+
direct: {
|
|
8
|
+
vendorId: string;
|
|
9
|
+
apiStyle: string;
|
|
10
|
+
baseUrlHint?: string | null;
|
|
11
|
+
modelId: string;
|
|
12
|
+
confidence: string;
|
|
13
|
+
notes: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface OpenRouterModel {
|
|
17
|
+
openrouterId: string;
|
|
18
|
+
canonicalSlug: string;
|
|
19
|
+
displayName: string;
|
|
20
|
+
vendor: OpenRouterModelVendor;
|
|
21
|
+
aliases: string[];
|
|
22
|
+
fallbackOf: string[];
|
|
23
|
+
capabilities: {
|
|
24
|
+
contextLength: number;
|
|
25
|
+
supportedParameters: string[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface OpenRouterCatalog {
|
|
29
|
+
schemaVersion: string;
|
|
30
|
+
source: {
|
|
31
|
+
providersEndpoint: string;
|
|
32
|
+
modelsEndpoint: string;
|
|
33
|
+
retrievedAt: string;
|
|
34
|
+
counts: {
|
|
35
|
+
providers: number;
|
|
36
|
+
models: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
providers: OpenRouterProvider[];
|
|
40
|
+
models: OpenRouterModel[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* OpenRouter Catalog Loader
|
|
44
|
+
* Loads and caches the OpenRouter catalog data from JSON file
|
|
45
|
+
*/
|
|
46
|
+
export declare class OpenRouterCatalogLoader {
|
|
47
|
+
private static instance;
|
|
48
|
+
private catalog;
|
|
49
|
+
private catalogPath;
|
|
50
|
+
private constructor();
|
|
51
|
+
static getInstance(): OpenRouterCatalogLoader;
|
|
52
|
+
/**
|
|
53
|
+
* Load the catalog from the JSON file
|
|
54
|
+
*/
|
|
55
|
+
load(): Promise<OpenRouterCatalog>;
|
|
56
|
+
/**
|
|
57
|
+
* Get the loaded catalog (throws if not loaded)
|
|
58
|
+
*/
|
|
59
|
+
getCatalog(): OpenRouterCatalog;
|
|
60
|
+
/**
|
|
61
|
+
* Get all providers
|
|
62
|
+
*/
|
|
63
|
+
getProviders(): Promise<OpenRouterProvider[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Get all models
|
|
66
|
+
*/
|
|
67
|
+
getModels(): Promise<OpenRouterModel[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Find a model by OpenRouter ID
|
|
70
|
+
*/
|
|
71
|
+
findModelById(openrouterId: string): Promise<OpenRouterModel | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Find models by canonical slug
|
|
74
|
+
*/
|
|
75
|
+
findModelByCanonicalSlug(canonicalSlug: string): Promise<OpenRouterModel | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Find models by aliases (returns all matches)
|
|
78
|
+
*/
|
|
79
|
+
findModelsByAlias(alias: string): Promise<OpenRouterModel[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Find provider by slug
|
|
82
|
+
*/
|
|
83
|
+
findProviderBySlug(slug: string): Promise<OpenRouterProvider | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Get all provider slugs
|
|
86
|
+
*/
|
|
87
|
+
getProviderSlugs(): Promise<string[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Find provider by vendor ID (from model.vendor.direct.vendorId)
|
|
90
|
+
* This maps vendor IDs to provider slugs for alias support
|
|
91
|
+
*/
|
|
92
|
+
findProviderByVendorId(vendorId: string): Promise<OpenRouterProvider | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Get all vendor IDs that map to providers
|
|
95
|
+
*/
|
|
96
|
+
getAllVendorIds(): Promise<string[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Check if a model is available
|
|
99
|
+
*/
|
|
100
|
+
isModelAvailable(modelId: string): Promise<boolean>;
|
|
101
|
+
/**
|
|
102
|
+
* Get model capabilities
|
|
103
|
+
*/
|
|
104
|
+
getModelCapabilities(modelId: string): Promise<OpenRouterModel['capabilities'] | null>;
|
|
105
|
+
/**
|
|
106
|
+
* Validate model supports a specific parameter
|
|
107
|
+
*/
|
|
108
|
+
modelSupportsParameter(modelId: string, parameter: string): Promise<boolean>;
|
|
109
|
+
/**
|
|
110
|
+
* Infer provider from model name using catalog data
|
|
111
|
+
*/
|
|
112
|
+
inferProviderFromModel(modelName: string): Promise<string | null>;
|
|
113
|
+
/**
|
|
114
|
+
* Normalize model name to OpenRouter format
|
|
115
|
+
* Returns the canonical OpenRouter ID for the model
|
|
116
|
+
*/
|
|
117
|
+
normalizeModelName(modelName: string, providerHint?: string): Promise<string>;
|
|
118
|
+
}
|
|
119
|
+
export declare const openRouterCatalog: OpenRouterCatalogLoader;
|