@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,744 @@
|
|
|
1
|
+
import { AIRouter } from './Router.js';
|
|
2
|
+
import { ProviderRegistry } from '../registry/ProviderRegistry.js';
|
|
3
|
+
import { AdapterRegistry } from '../registry/AdapterRegistry.js';
|
|
4
|
+
import { OpenAIAdapter } from '../adapters/openai/OpenAIAdapter.js';
|
|
5
|
+
import { GrokAdapter } from '../adapters/grok/GrokAdapter.js';
|
|
6
|
+
import { OpenRouterAdapter } from '../adapters/openrouter/OpenRouterAdapter.js';
|
|
7
|
+
import { getLogger } from '../logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve ENV. placeholders to actual environment variable values
|
|
10
|
+
* Replaces nx-config2 functionality to avoid ESM/CommonJS compatibility issues
|
|
11
|
+
*/
|
|
12
|
+
function resolveEnvPlaceholders(config) {
|
|
13
|
+
if (typeof config === 'string') {
|
|
14
|
+
// Handle ENV.VAR_NAME||default pattern
|
|
15
|
+
if (config.startsWith('ENV.')) {
|
|
16
|
+
const match = config.match(/^ENV\.([^|]+)(?:\|\|(.+))?$/);
|
|
17
|
+
if (match) {
|
|
18
|
+
const envVar = match[1];
|
|
19
|
+
const defaultValue = match[2] !== undefined ? match[2] : undefined;
|
|
20
|
+
const value = process.env[envVar];
|
|
21
|
+
return value !== undefined ? value : (defaultValue !== undefined ? defaultValue : config);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(config)) {
|
|
27
|
+
return config.map(item => resolveEnvPlaceholders(item));
|
|
28
|
+
}
|
|
29
|
+
if (config && typeof config === 'object') {
|
|
30
|
+
const resolved = {};
|
|
31
|
+
for (const [key, value] of Object.entries(config)) {
|
|
32
|
+
resolved[key] = resolveEnvPlaceholders(value);
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wrapper around AIRouter that adds logging, interceptors, and usage tracking
|
|
40
|
+
* Maintains backward compatibility with existing router API
|
|
41
|
+
*/
|
|
42
|
+
export class LLMProviderRouter {
|
|
43
|
+
constructor(config) {
|
|
44
|
+
this.lastConfiguredProvider = null;
|
|
45
|
+
this.autoRegistrationAttempted = false;
|
|
46
|
+
this.config = config || {};
|
|
47
|
+
this.requestInterceptors = [];
|
|
48
|
+
this.responseInterceptors = [];
|
|
49
|
+
this.providerConfigs = new Map();
|
|
50
|
+
// Initialize logger
|
|
51
|
+
this.logger = config?.logger || getLogger({
|
|
52
|
+
verbose: config?.verbose || false,
|
|
53
|
+
level: config?.logLevel || 'info',
|
|
54
|
+
});
|
|
55
|
+
// Initialize registries
|
|
56
|
+
this.providerRegistry = new ProviderRegistry();
|
|
57
|
+
this.adapterRegistry = new AdapterRegistry();
|
|
58
|
+
// Register default adapters
|
|
59
|
+
this.adapterRegistry.register(new OpenAIAdapter());
|
|
60
|
+
this.adapterRegistry.register(new GrokAdapter());
|
|
61
|
+
this.adapterRegistry.register(new OpenRouterAdapter());
|
|
62
|
+
// Create router
|
|
63
|
+
this.router = new AIRouter(this.providerRegistry, this.adapterRegistry);
|
|
64
|
+
this.logger.info('Router initialized with ProviderModule architecture', {
|
|
65
|
+
verbose: this.logger.verbose,
|
|
66
|
+
logLevel: this.logger.level,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Register a provider module
|
|
71
|
+
*/
|
|
72
|
+
registerProvider(providerModuleOrFactory, factoryName) {
|
|
73
|
+
let providerModule;
|
|
74
|
+
// Check if it's already a ProviderModule
|
|
75
|
+
if (providerModuleOrFactory.name && providerModuleOrFactory.execute) {
|
|
76
|
+
providerModule = providerModuleOrFactory;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// It's a factory module - extract the factory function
|
|
80
|
+
let factory;
|
|
81
|
+
if (factoryName && providerModuleOrFactory[factoryName]) {
|
|
82
|
+
factory = providerModuleOrFactory[factoryName];
|
|
83
|
+
}
|
|
84
|
+
else if (providerModuleOrFactory.initializeClient) {
|
|
85
|
+
factory = providerModuleOrFactory.initializeClient;
|
|
86
|
+
}
|
|
87
|
+
else if (providerModuleOrFactory.createOpenAIProvider) {
|
|
88
|
+
factory = providerModuleOrFactory.createOpenAIProvider;
|
|
89
|
+
}
|
|
90
|
+
else if (typeof providerModuleOrFactory.default === 'function') {
|
|
91
|
+
factory = providerModuleOrFactory.default;
|
|
92
|
+
}
|
|
93
|
+
else if (providerModuleOrFactory.provider && typeof providerModuleOrFactory.provider === 'object') {
|
|
94
|
+
// Grok-style: export const provider: ProviderModule
|
|
95
|
+
providerModule = providerModuleOrFactory.provider;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
throw new Error(`Cannot determine how to extract ProviderModule from provider. Available keys: ${Object.keys(providerModuleOrFactory).join(', ')}`);
|
|
99
|
+
}
|
|
100
|
+
if (factory) {
|
|
101
|
+
// Get config for this provider
|
|
102
|
+
// Determine provider name based on factory name or available configs
|
|
103
|
+
let providerName;
|
|
104
|
+
if (factoryName === 'createOpenAIProvider') {
|
|
105
|
+
providerName = 'openai';
|
|
106
|
+
}
|
|
107
|
+
else if (factoryName === 'initializeClient') {
|
|
108
|
+
// Use the most recently configured provider (set right before registerProvider is called)
|
|
109
|
+
if (this.lastConfiguredProvider && this.providerConfigs.has(this.lastConfiguredProvider)) {
|
|
110
|
+
providerName = this.lastConfiguredProvider;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Fallback: try common providers in order
|
|
114
|
+
if (this.providerConfigs.has('openai')) {
|
|
115
|
+
providerName = 'openai';
|
|
116
|
+
}
|
|
117
|
+
else if (this.providerConfigs.has('grok')) {
|
|
118
|
+
providerName = 'grok';
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Use first available config
|
|
122
|
+
const configKeys = Array.from(this.providerConfigs.keys());
|
|
123
|
+
providerName = configKeys.length > 0 ? configKeys[0] : 'unknown';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
providerName = 'unknown';
|
|
129
|
+
}
|
|
130
|
+
const config = this.providerConfigs.get(providerName) || {};
|
|
131
|
+
providerModule = factory(config);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!providerModule || !providerModule.name || !providerModule.execute) {
|
|
135
|
+
throw new Error('Failed to create ProviderModule from factory');
|
|
136
|
+
}
|
|
137
|
+
// Register the provider module
|
|
138
|
+
this.providerRegistry.register(providerModule);
|
|
139
|
+
// Ensure adapter exists for this provider (register if missing)
|
|
140
|
+
// Adapters are registered by their provider field, which should match ProviderModule.name
|
|
141
|
+
const providerName = providerModule.name;
|
|
142
|
+
if (!this.adapterRegistry.has(providerName)) {
|
|
143
|
+
// Try to find a matching adapter or create a default one
|
|
144
|
+
// For now, we assume adapters are pre-registered, but we could add dynamic adapter creation here
|
|
145
|
+
this.logger.warn(`No adapter found for provider '${providerName}'. Make sure adapter is registered.`);
|
|
146
|
+
}
|
|
147
|
+
this.logger.info('Provider registered', { provider: providerModule.name });
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Configure provider SDK client config
|
|
151
|
+
*/
|
|
152
|
+
configureProvider(providerName, config) {
|
|
153
|
+
this.providerConfigs.set(providerName, config);
|
|
154
|
+
this.lastConfiguredProvider = providerName; // Track most recently configured provider
|
|
155
|
+
this.logger.debug('Provider configuration set', { provider: providerName });
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Add request interceptor
|
|
159
|
+
*/
|
|
160
|
+
addRequestInterceptor(interceptor) {
|
|
161
|
+
this.requestInterceptors.push(interceptor);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Add response interceptor
|
|
165
|
+
*/
|
|
166
|
+
addResponseInterceptor(interceptor) {
|
|
167
|
+
this.responseInterceptors.push(interceptor);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Invoke router (sync mode)
|
|
171
|
+
*/
|
|
172
|
+
async invoke(request) {
|
|
173
|
+
const startTime = Date.now();
|
|
174
|
+
// Auto-register providers if needed
|
|
175
|
+
await this.ensureProvidersRegistered();
|
|
176
|
+
// Apply request interceptors
|
|
177
|
+
let processedRequest = request;
|
|
178
|
+
for (const interceptor of this.requestInterceptors) {
|
|
179
|
+
processedRequest = await interceptor(processedRequest, request.provider);
|
|
180
|
+
}
|
|
181
|
+
// Ensure exec.timeoutMs is always set (router owns execution semantics)
|
|
182
|
+
if (!processedRequest.exec) {
|
|
183
|
+
processedRequest.exec = {};
|
|
184
|
+
}
|
|
185
|
+
if (processedRequest.exec.timeoutMs === undefined) {
|
|
186
|
+
processedRequest.exec.timeoutMs = this.config.timeoutMs ?? 60000;
|
|
187
|
+
}
|
|
188
|
+
// Log request
|
|
189
|
+
this.logger.logAIRequest(request.provider || 'unknown', processedRequest, {
|
|
190
|
+
requestId: processedRequest.requestId,
|
|
191
|
+
});
|
|
192
|
+
// Execute
|
|
193
|
+
const invokeStartTime = Date.now();
|
|
194
|
+
let result;
|
|
195
|
+
try {
|
|
196
|
+
result = await this.router.runSync(processedRequest);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// Properly extract error message
|
|
200
|
+
let err;
|
|
201
|
+
if (error instanceof Error) {
|
|
202
|
+
err = error;
|
|
203
|
+
}
|
|
204
|
+
else if (error && typeof error === 'object' && 'message' in error) {
|
|
205
|
+
err = new Error(String(error.message));
|
|
206
|
+
if ('stack' in error) {
|
|
207
|
+
err.stack = String(error.stack);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
err = new Error(String(error));
|
|
212
|
+
}
|
|
213
|
+
this.logger.error('Router execution failed', {
|
|
214
|
+
provider: request.provider,
|
|
215
|
+
error: err.message,
|
|
216
|
+
stack: err.stack,
|
|
217
|
+
});
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
const invokeDuration = Date.now() - invokeStartTime;
|
|
221
|
+
// Log response
|
|
222
|
+
this.logger.logAIResponse(result.provider, result, {
|
|
223
|
+
requestId: result.requestId,
|
|
224
|
+
duration: invokeDuration,
|
|
225
|
+
usage: result.usage,
|
|
226
|
+
});
|
|
227
|
+
// #region agent log
|
|
228
|
+
const logDataC = { location: 'RouterWrapper.ts:224', message: 'Before response interceptors', data: { hasMetadata: !!result.metadata, metadataKeys: result.metadata ? Object.keys(result.metadata) : [], hasAiActivitiesResponse: !!result.metadata?.['ai-activities-response'], hasAiActivitiesRequest: !!result.metadata?.['ai-activities-request'], interceptorsCount: this.responseInterceptors.length }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'C' };
|
|
229
|
+
console.log('[DEBUG]', logDataC);
|
|
230
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logDataC) }).catch(() => { });
|
|
231
|
+
// #endregion
|
|
232
|
+
// Apply response interceptors
|
|
233
|
+
let processedResponse = result;
|
|
234
|
+
for (let i = 0; i < this.responseInterceptors.length; i++) {
|
|
235
|
+
const interceptor = this.responseInterceptors[i];
|
|
236
|
+
const beforeInterceptor = {
|
|
237
|
+
hasMetadata: !!processedResponse.metadata,
|
|
238
|
+
metadataKeys: processedResponse.metadata ? Object.keys(processedResponse.metadata) : [],
|
|
239
|
+
hasAiActivitiesResponse: !!processedResponse.metadata?.['ai-activities-response'],
|
|
240
|
+
hasAiActivitiesRequest: !!processedResponse.metadata?.['ai-activities-request'],
|
|
241
|
+
};
|
|
242
|
+
processedResponse = await interceptor(processedResponse, result.provider);
|
|
243
|
+
const afterInterceptor = {
|
|
244
|
+
hasMetadata: !!processedResponse.metadata,
|
|
245
|
+
metadataKeys: processedResponse.metadata ? Object.keys(processedResponse.metadata) : [],
|
|
246
|
+
hasAiActivitiesResponse: !!processedResponse.metadata?.['ai-activities-response'],
|
|
247
|
+
hasAiActivitiesRequest: !!processedResponse.metadata?.['ai-activities-request'],
|
|
248
|
+
};
|
|
249
|
+
// #region agent log
|
|
250
|
+
const interceptorLog = { location: `RouterWrapper.ts:231:interceptor_${i}`, message: `Interceptor ${i} execution`, data: { before: beforeInterceptor, after: afterInterceptor, changed: JSON.stringify(beforeInterceptor) !== JSON.stringify(afterInterceptor) }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: `INTERCEPTOR_${i}` };
|
|
251
|
+
console.log(`[DEBUG RouterWrapper] Interceptor ${i} - Before:`, JSON.stringify(beforeInterceptor, null, 2));
|
|
252
|
+
console.log(`[DEBUG RouterWrapper] Interceptor ${i} - After:`, JSON.stringify(afterInterceptor, null, 2));
|
|
253
|
+
if (JSON.stringify(beforeInterceptor) !== JSON.stringify(afterInterceptor)) {
|
|
254
|
+
console.warn(`[WARNING RouterWrapper] Interceptor ${i} modified ai-activities metadata!`);
|
|
255
|
+
}
|
|
256
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(interceptorLog) }).catch(() => { });
|
|
257
|
+
// #endregion
|
|
258
|
+
}
|
|
259
|
+
// #region agent log
|
|
260
|
+
const logDataD = { location: 'RouterWrapper.ts:230', message: 'After response interceptors', data: { hasMetadata: !!processedResponse.metadata, metadataKeys: processedResponse.metadata ? Object.keys(processedResponse.metadata) : [], hasAiActivitiesResponse: !!processedResponse.metadata?.['ai-activities-response'], hasAiActivitiesRequest: !!processedResponse.metadata?.['ai-activities-request'], responseChanged: processedResponse !== result }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'D' };
|
|
261
|
+
console.log('[DEBUG]', logDataD);
|
|
262
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logDataD) }).catch(() => { });
|
|
263
|
+
// #endregion
|
|
264
|
+
// Record usage
|
|
265
|
+
if (this.config.usageTracker) {
|
|
266
|
+
const duration = Date.now() - startTime;
|
|
267
|
+
this.config.usageTracker.recordRequest({
|
|
268
|
+
provider: result.provider,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
duration,
|
|
271
|
+
tokens: {
|
|
272
|
+
input: result.usage?.inputTokens || 0,
|
|
273
|
+
output: result.usage?.outputTokens || 0,
|
|
274
|
+
total: result.usage?.totalTokens || 0,
|
|
275
|
+
},
|
|
276
|
+
cost: result.usage?.cost,
|
|
277
|
+
success: true,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// #region agent log
|
|
281
|
+
const logDataE = { location: 'RouterWrapper.ts:246', message: 'Returning final response from invoke', data: { hasMetadata: !!processedResponse.metadata, metadataKeys: processedResponse.metadata ? Object.keys(processedResponse.metadata) : [], hasAiActivitiesResponse: !!processedResponse.metadata?.['ai-activities-response'], hasAiActivitiesRequest: !!processedResponse.metadata?.['ai-activities-request'], finalResponseKeys: Object.keys(processedResponse) }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'E' };
|
|
282
|
+
console.log('[DEBUG RouterWrapper] Final response metadata keys:', processedResponse.metadata ? Object.keys(processedResponse.metadata) : 'NO METADATA');
|
|
283
|
+
console.log('[DEBUG RouterWrapper] ai-activities keys in final response:', processedResponse.metadata ? Object.keys(processedResponse.metadata).filter(k => k.startsWith('ai-activities')) : []);
|
|
284
|
+
console.log('[DEBUG RouterWrapper] Full log data:', JSON.stringify(logDataE, null, 2));
|
|
285
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logDataE) }).catch(() => { });
|
|
286
|
+
// Test serialization to verify metadata survives JSON.stringify/parse
|
|
287
|
+
let serializationTest = null;
|
|
288
|
+
let serializationError = null;
|
|
289
|
+
try {
|
|
290
|
+
const serialized = JSON.stringify(processedResponse);
|
|
291
|
+
const deserialized = JSON.parse(serialized);
|
|
292
|
+
serializationTest = {
|
|
293
|
+
success: true,
|
|
294
|
+
hasMetadataAfterSerialize: !!deserialized.metadata,
|
|
295
|
+
metadataKeysAfterSerialize: deserialized.metadata ? Object.keys(deserialized.metadata) : [],
|
|
296
|
+
hasAiActivitiesResponseAfterSerialize: !!deserialized.metadata?.['ai-activities-response'],
|
|
297
|
+
hasAiActivitiesRequestAfterSerialize: !!deserialized.metadata?.['ai-activities-request'],
|
|
298
|
+
aiActivitiesKeysAfterSerialize: deserialized.metadata ? Object.keys(deserialized.metadata).filter((k) => k.startsWith('ai-activities')) : [],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
serializationError = err.message || String(err);
|
|
303
|
+
serializationTest = { success: false, error: serializationError };
|
|
304
|
+
}
|
|
305
|
+
const logDataF = { location: 'RouterWrapper.ts:280', message: 'Serialization test of final response', data: serializationTest, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'F' };
|
|
306
|
+
console.log('[DEBUG RouterWrapper] Serialization test:', JSON.stringify(serializationTest, null, 2));
|
|
307
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logDataF) }).catch(() => { });
|
|
308
|
+
// Detailed inspection of ai-activities data structure
|
|
309
|
+
const aiActivitiesInspection = {
|
|
310
|
+
hasAiActivitiesResponse: !!processedResponse.metadata?.['ai-activities-response'],
|
|
311
|
+
hasAiActivitiesRequest: !!processedResponse.metadata?.['ai-activities-request'],
|
|
312
|
+
aiActivitiesResponseType: typeof processedResponse.metadata?.['ai-activities-response'],
|
|
313
|
+
aiActivitiesRequestType: typeof processedResponse.metadata?.['ai-activities-request'],
|
|
314
|
+
};
|
|
315
|
+
if (processedResponse.metadata?.['ai-activities-response']) {
|
|
316
|
+
const resp = processedResponse.metadata['ai-activities-response'];
|
|
317
|
+
aiActivitiesInspection.aiActivitiesResponseKeys = Object.keys(resp);
|
|
318
|
+
aiActivitiesInspection.hasRequestId = !!resp.requestId;
|
|
319
|
+
aiActivitiesInspection.hasProvider = !!resp.provider;
|
|
320
|
+
aiActivitiesInspection.hasRawResponse = !!resp.rawResponse;
|
|
321
|
+
aiActivitiesInspection.hasOutputText = !!resp.outputText;
|
|
322
|
+
aiActivitiesInspection.hasUsage = !!resp.usage;
|
|
323
|
+
aiActivitiesInspection.hasReasoning = !!resp.reasoning;
|
|
324
|
+
aiActivitiesInspection.outputTextLength = resp.outputText?.length || 0;
|
|
325
|
+
aiActivitiesInspection.outputTextPreview = resp.outputText?.substring(0, 100) || null;
|
|
326
|
+
}
|
|
327
|
+
if (processedResponse.metadata?.['ai-activities-request']) {
|
|
328
|
+
const req = processedResponse.metadata['ai-activities-request'];
|
|
329
|
+
aiActivitiesInspection.aiActivitiesRequestKeys = Object.keys(req);
|
|
330
|
+
aiActivitiesInspection.requestHasInstructions = !!req.instructions;
|
|
331
|
+
aiActivitiesInspection.requestHasInputData = !!req.inputData;
|
|
332
|
+
aiActivitiesInspection.requestHasConfig = !!req.config;
|
|
333
|
+
aiActivitiesInspection.requestHasCallSpec = !!req._callSpec;
|
|
334
|
+
}
|
|
335
|
+
// Test if we can serialize just the ai-activities data
|
|
336
|
+
let aiActivitiesSerializationTest = null;
|
|
337
|
+
try {
|
|
338
|
+
const aiActivitiesOnly = {
|
|
339
|
+
'ai-activities-response': processedResponse.metadata?.['ai-activities-response'],
|
|
340
|
+
'ai-activities-request': processedResponse.metadata?.['ai-activities-request'],
|
|
341
|
+
'ai-activities-raw-response': processedResponse.metadata?.['ai-activities-raw-response'],
|
|
342
|
+
'ai-activities-original-response': processedResponse.metadata?.['ai-activities-original-response'],
|
|
343
|
+
'ai-activities-exec-meta': processedResponse.metadata?.['ai-activities-exec-meta'],
|
|
344
|
+
};
|
|
345
|
+
const serialized = JSON.stringify(aiActivitiesOnly);
|
|
346
|
+
const deserialized = JSON.parse(serialized);
|
|
347
|
+
aiActivitiesSerializationTest = {
|
|
348
|
+
success: true,
|
|
349
|
+
serializedSize: serialized.length,
|
|
350
|
+
hasResponseAfterSerialize: !!deserialized['ai-activities-response'],
|
|
351
|
+
hasRequestAfterSerialize: !!deserialized['ai-activities-request'],
|
|
352
|
+
responseKeysAfterSerialize: deserialized['ai-activities-response'] ? Object.keys(deserialized['ai-activities-response']) : [],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
aiActivitiesSerializationTest = { success: false, error: err.message || String(err) };
|
|
357
|
+
}
|
|
358
|
+
const logDataG = { location: 'RouterWrapper.ts:320', message: 'Detailed ai-activities inspection', data: { inspection: aiActivitiesInspection, serializationTest: aiActivitiesSerializationTest }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'G' };
|
|
359
|
+
console.log('[DEBUG RouterWrapper] ai-activities inspection:', JSON.stringify(aiActivitiesInspection, null, 2));
|
|
360
|
+
console.log('[DEBUG RouterWrapper] ai-activities serialization test:', JSON.stringify(aiActivitiesSerializationTest, null, 2));
|
|
361
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logDataG) }).catch(() => { });
|
|
362
|
+
// Summary: Print complete response structure for downstream system debugging
|
|
363
|
+
console.log('\n=== ROUTER RESPONSE SUMMARY (for downstream system) ===');
|
|
364
|
+
console.log('Response has metadata:', !!processedResponse.metadata);
|
|
365
|
+
console.log('Metadata keys:', processedResponse.metadata ? Object.keys(processedResponse.metadata) : 'NONE');
|
|
366
|
+
console.log('ai-activities keys:', processedResponse.metadata ? Object.keys(processedResponse.metadata).filter(k => k.startsWith('ai-activities')) : []);
|
|
367
|
+
console.log('Access path: response.metadata["ai-activities-response"]');
|
|
368
|
+
console.log('Access path: response.metadata["ai-activities-request"]');
|
|
369
|
+
if (processedResponse.metadata?.['ai-activities-response']) {
|
|
370
|
+
const resp = processedResponse.metadata['ai-activities-response'];
|
|
371
|
+
console.log('ai-activities-response structure:', {
|
|
372
|
+
hasRequestId: !!resp.requestId,
|
|
373
|
+
hasProvider: !!resp.provider,
|
|
374
|
+
hasRawResponse: !!resp.rawResponse,
|
|
375
|
+
hasOutputText: !!resp.outputText,
|
|
376
|
+
hasUsage: !!resp.usage,
|
|
377
|
+
hasReasoning: !!resp.reasoning,
|
|
378
|
+
outputTextPreview: resp.outputText?.substring(0, 50) || 'NONE',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
console.log('=== END ROUTER RESPONSE SUMMARY ===\n');
|
|
382
|
+
// Final verification: Test accessing ai-activities data as downstream system would
|
|
383
|
+
try {
|
|
384
|
+
const testAccess = {
|
|
385
|
+
'ai-activities-response': processedResponse.metadata?.['ai-activities-response'],
|
|
386
|
+
'ai-activities-request': processedResponse.metadata?.['ai-activities-request'],
|
|
387
|
+
'ai-activities-raw-response': processedResponse.metadata?.['ai-activities-raw-response'],
|
|
388
|
+
'ai-activities-original-response': processedResponse.metadata?.['ai-activities-original-response'],
|
|
389
|
+
'ai-activities-exec-meta': processedResponse.metadata?.['ai-activities-exec-meta'],
|
|
390
|
+
};
|
|
391
|
+
const canAccess = {
|
|
392
|
+
responseExists: !!testAccess['ai-activities-response'],
|
|
393
|
+
requestExists: !!testAccess['ai-activities-request'],
|
|
394
|
+
responseHasData: testAccess['ai-activities-response'] ? Object.keys(testAccess['ai-activities-response']).length > 0 : false,
|
|
395
|
+
requestHasData: testAccess['ai-activities-request'] ? Object.keys(testAccess['ai-activities-request']).length > 0 : false,
|
|
396
|
+
};
|
|
397
|
+
console.log('[VERIFICATION] Downstream access test:', JSON.stringify(canAccess, null, 2));
|
|
398
|
+
console.log('[VERIFICATION] Example access code:');
|
|
399
|
+
console.log(' const response = await router.invoke(request);');
|
|
400
|
+
console.log(' const activitiesResponse = response.metadata["ai-activities-response"];');
|
|
401
|
+
console.log(' const activitiesRequest = response.metadata["ai-activities-request"];');
|
|
402
|
+
console.log(' // Save to database: activitiesResponse, activitiesRequest');
|
|
403
|
+
if (!canAccess.responseExists || !canAccess.requestExists) {
|
|
404
|
+
console.error('[ERROR] ai-activities data is missing! This will cause database saves to fail.');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
console.error('[ERROR] Failed to verify ai-activities access:', err.message);
|
|
409
|
+
}
|
|
410
|
+
// Final safeguard: Ensure metadata is never lost
|
|
411
|
+
if (!processedResponse.metadata) {
|
|
412
|
+
console.error('[CRITICAL ERROR] Response has no metadata! Creating empty metadata object.');
|
|
413
|
+
processedResponse.metadata = {};
|
|
414
|
+
}
|
|
415
|
+
// Final safeguard: Ensure ai-activities data exists, log warning if missing
|
|
416
|
+
if (!processedResponse.metadata['ai-activities-response'] || !processedResponse.metadata['ai-activities-request']) {
|
|
417
|
+
console.warn('[WARNING] ai-activities data is missing from final response!');
|
|
418
|
+
console.warn('[WARNING] This will cause database saves to fail.');
|
|
419
|
+
console.warn('[WARNING] Metadata keys present:', Object.keys(processedResponse.metadata));
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.log('[SUCCESS] ai-activities data confirmed present in final response');
|
|
423
|
+
}
|
|
424
|
+
// Final log: Complete response structure snapshot
|
|
425
|
+
const finalSnapshot = {
|
|
426
|
+
timestamp: new Date().toISOString(),
|
|
427
|
+
hasMetadata: !!processedResponse.metadata,
|
|
428
|
+
metadataKeys: processedResponse.metadata ? Object.keys(processedResponse.metadata) : [],
|
|
429
|
+
aiActivitiesKeys: processedResponse.metadata ? Object.keys(processedResponse.metadata).filter(k => k.startsWith('ai-activities')) : [],
|
|
430
|
+
hasAiActivitiesResponse: !!processedResponse.metadata?.['ai-activities-response'],
|
|
431
|
+
hasAiActivitiesRequest: !!processedResponse.metadata?.['ai-activities-request'],
|
|
432
|
+
responseKeys: Object.keys(processedResponse),
|
|
433
|
+
};
|
|
434
|
+
console.log('[FINAL SNAPSHOT] Response structure before return:', JSON.stringify(finalSnapshot, null, 2));
|
|
435
|
+
// Add helper property for easier access (non-enumerable to avoid breaking serialization)
|
|
436
|
+
// This makes it easier for downstream systems to find the data
|
|
437
|
+
if (processedResponse.metadata?.['ai-activities-response'] && processedResponse.metadata?.['ai-activities-request']) {
|
|
438
|
+
try {
|
|
439
|
+
Object.defineProperty(processedResponse, '_aiActivities', {
|
|
440
|
+
value: {
|
|
441
|
+
response: processedResponse.metadata['ai-activities-response'],
|
|
442
|
+
request: processedResponse.metadata['ai-activities-request'],
|
|
443
|
+
rawResponse: processedResponse.metadata['ai-activities-raw-response'],
|
|
444
|
+
originalResponse: processedResponse.metadata['ai-activities-original-response'],
|
|
445
|
+
execMeta: processedResponse.metadata['ai-activities-exec-meta'],
|
|
446
|
+
},
|
|
447
|
+
enumerable: false, // Don't include in JSON.stringify
|
|
448
|
+
writable: false,
|
|
449
|
+
configurable: true,
|
|
450
|
+
});
|
|
451
|
+
console.log('[HELPER] Added _aiActivities helper property (access via response._aiActivities)');
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
console.warn('[WARNING] Could not add _aiActivities helper property:', err.message);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// #endregion
|
|
458
|
+
return processedResponse;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Stream request
|
|
462
|
+
*/
|
|
463
|
+
async *stream(request) {
|
|
464
|
+
// Auto-register providers if needed
|
|
465
|
+
await this.ensureProvidersRegistered();
|
|
466
|
+
// Apply request interceptors
|
|
467
|
+
let processedRequest = request;
|
|
468
|
+
for (const interceptor of this.requestInterceptors) {
|
|
469
|
+
processedRequest = await interceptor(processedRequest, request.provider);
|
|
470
|
+
}
|
|
471
|
+
// Ensure exec.timeoutMs is always set (router owns execution semantics)
|
|
472
|
+
// Router forwards all exec options (timeoutMs, idempotencyKey, signal) to providers
|
|
473
|
+
if (!processedRequest.exec) {
|
|
474
|
+
processedRequest.exec = {};
|
|
475
|
+
}
|
|
476
|
+
if (processedRequest.exec.timeoutMs === undefined) {
|
|
477
|
+
processedRequest.exec.timeoutMs = this.config.timeoutMs ?? this.config.defaultTimeoutMs ?? 60000;
|
|
478
|
+
}
|
|
479
|
+
// idempotencyKey and signal are passed through as-is if provided
|
|
480
|
+
// Log request
|
|
481
|
+
this.logger.logAIRequest(request.provider || 'unknown', processedRequest, {
|
|
482
|
+
requestId: processedRequest.requestId,
|
|
483
|
+
});
|
|
484
|
+
// Stream
|
|
485
|
+
for await (const event of this.router.runStream(processedRequest)) {
|
|
486
|
+
// Apply response interceptors to completed events
|
|
487
|
+
if (event.type === 'completed') {
|
|
488
|
+
let processedResponse = event.response;
|
|
489
|
+
for (const interceptor of this.responseInterceptors) {
|
|
490
|
+
processedResponse = await interceptor(processedResponse, event.response.provider);
|
|
491
|
+
}
|
|
492
|
+
yield {
|
|
493
|
+
type: 'completed',
|
|
494
|
+
requestId: event.requestId,
|
|
495
|
+
response: processedResponse,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
yield event;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Batch request
|
|
505
|
+
*/
|
|
506
|
+
async createBatch(providerName, items, exec) {
|
|
507
|
+
// Auto-register providers if needed
|
|
508
|
+
await this.ensureProvidersRegistered();
|
|
509
|
+
// Ensure exec.timeoutMs is always set (router owns execution semantics)
|
|
510
|
+
// Router forwards all exec options (timeoutMs, idempotencyKey, signal) to providers
|
|
511
|
+
const execWithDefaults = {
|
|
512
|
+
timeoutMs: exec?.timeoutMs ?? this.config.timeoutMs ?? this.config.defaultTimeoutMs ?? 60000,
|
|
513
|
+
retries: exec?.retries,
|
|
514
|
+
idempotencyKey: exec?.idempotencyKey,
|
|
515
|
+
signal: exec?.signal,
|
|
516
|
+
};
|
|
517
|
+
return this.router.runBatch(providerName, items, execWithDefaults);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* List registered providers
|
|
521
|
+
*/
|
|
522
|
+
listProviders() {
|
|
523
|
+
return this.providerRegistry.list();
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Check health of a specific provider
|
|
527
|
+
*/
|
|
528
|
+
async checkHealth(provider) {
|
|
529
|
+
const startTime = Date.now();
|
|
530
|
+
try {
|
|
531
|
+
// Try a minimal health check request
|
|
532
|
+
const testRequest = {
|
|
533
|
+
request: {
|
|
534
|
+
instructions: '',
|
|
535
|
+
inputData: 'ping',
|
|
536
|
+
},
|
|
537
|
+
provider,
|
|
538
|
+
mode: 'sync',
|
|
539
|
+
exec: {
|
|
540
|
+
timeoutMs: 5000,
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
this.logger.debug('Performing health check', { provider });
|
|
544
|
+
const result = await this.invoke(testRequest);
|
|
545
|
+
const latencyMs = Date.now() - startTime;
|
|
546
|
+
this.logger.debug('Health check passed', { provider, latencyMs });
|
|
547
|
+
return {
|
|
548
|
+
provider,
|
|
549
|
+
healthy: true,
|
|
550
|
+
latencyMs,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
const latencyMs = Date.now() - startTime;
|
|
555
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
556
|
+
this.logger.warn('Health check failed', {
|
|
557
|
+
provider,
|
|
558
|
+
latencyMs,
|
|
559
|
+
error: errorMessage,
|
|
560
|
+
});
|
|
561
|
+
return {
|
|
562
|
+
provider,
|
|
563
|
+
healthy: false,
|
|
564
|
+
latencyMs,
|
|
565
|
+
error: errorMessage,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get provider registry (for advanced usage)
|
|
571
|
+
*/
|
|
572
|
+
getProviderRegistry() {
|
|
573
|
+
return this.providerRegistry;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get adapter registry (for advanced usage)
|
|
577
|
+
*/
|
|
578
|
+
getAdapterRegistry() {
|
|
579
|
+
return this.adapterRegistry;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Try to register OpenRouter if a key is available (constructor config or resolved env).
|
|
583
|
+
* Used by ensureProvidersRegistered() and by the OpenRouter retry path when the first run skipped registration.
|
|
584
|
+
*/
|
|
585
|
+
async tryRegisterOpenRouter(config) {
|
|
586
|
+
const openRouterKey = (this.config?.openrouter?.apiKey && typeof this.config.openrouter.apiKey === 'string' && !this.config.openrouter.apiKey.startsWith('ENV.'))
|
|
587
|
+
? this.config.openrouter.apiKey
|
|
588
|
+
: config.openrouter?.apiKey;
|
|
589
|
+
const hasOpenRouterKey = openRouterKey &&
|
|
590
|
+
typeof openRouterKey === 'string' &&
|
|
591
|
+
openRouterKey.trim() !== '' &&
|
|
592
|
+
!openRouterKey.startsWith('ENV.');
|
|
593
|
+
if (!hasOpenRouterKey || this.providerRegistry.has('openrouter'))
|
|
594
|
+
return;
|
|
595
|
+
try {
|
|
596
|
+
const { createOpenRouterProvider } = await import('../providers/OpenRouterProvider.js');
|
|
597
|
+
const openRouterConfig = {
|
|
598
|
+
apiKey: openRouterKey,
|
|
599
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
600
|
+
timeoutMs: this.config.timeoutMs ?? 60000,
|
|
601
|
+
httpReferer: config.openrouter?.httpReferer,
|
|
602
|
+
xTitle: config.openrouter?.xTitle,
|
|
603
|
+
};
|
|
604
|
+
this.configureProvider('openrouter', openRouterConfig);
|
|
605
|
+
const providerModule = createOpenRouterProvider(openRouterConfig);
|
|
606
|
+
if (providerModule) {
|
|
607
|
+
this.providerRegistry.register(providerModule);
|
|
608
|
+
this.addRequestInterceptor(async (req, originalProvider) => {
|
|
609
|
+
const newRequest = { ...req };
|
|
610
|
+
if (!newRequest.request)
|
|
611
|
+
newRequest.request = {};
|
|
612
|
+
if (!newRequest.request.config)
|
|
613
|
+
newRequest.request.config = {};
|
|
614
|
+
const callerProvider = originalProvider || newRequest.provider;
|
|
615
|
+
const allowOpenRouterProxy = newRequest.request.config?.allowOpenRouterProxy === true ||
|
|
616
|
+
newRequest.request.config?.providerProxy === 'openrouter';
|
|
617
|
+
if (callerProvider && callerProvider !== 'openrouter' && !allowOpenRouterProxy) {
|
|
618
|
+
newRequest.provider = callerProvider;
|
|
619
|
+
newRequest.request.config.provider = callerProvider;
|
|
620
|
+
return newRequest;
|
|
621
|
+
}
|
|
622
|
+
let effectiveProvider = originalProvider;
|
|
623
|
+
if (!effectiveProvider) {
|
|
624
|
+
const model = newRequest.request.config?.model || newRequest.request?.model;
|
|
625
|
+
if (model && typeof model === 'string') {
|
|
626
|
+
if (model.startsWith('gpt-') || model.startsWith('openai/'))
|
|
627
|
+
effectiveProvider = 'openai';
|
|
628
|
+
else if (model.startsWith('claude-') || model.startsWith('anthropic/'))
|
|
629
|
+
effectiveProvider = 'anthropic';
|
|
630
|
+
else if (model.startsWith('grok-') || model.startsWith('xai/'))
|
|
631
|
+
effectiveProvider = 'grok';
|
|
632
|
+
else if (model.startsWith('gemini-') || model.startsWith('google/'))
|
|
633
|
+
effectiveProvider = 'google';
|
|
634
|
+
}
|
|
635
|
+
if (!effectiveProvider)
|
|
636
|
+
effectiveProvider = 'openai';
|
|
637
|
+
}
|
|
638
|
+
newRequest.request.config.provider = effectiveProvider;
|
|
639
|
+
if (effectiveProvider !== 'openrouter') {
|
|
640
|
+
this.logger?.warn?.(`[OpenRouterProxy] Forcing provider=openrouter (effectiveProvider=${effectiveProvider}, originalProvider=${originalProvider ?? newRequest.provider}, model=${newRequest.request?.config?.model ?? newRequest.request?.model ?? 'unknown'})`);
|
|
641
|
+
newRequest.provider = 'openrouter';
|
|
642
|
+
}
|
|
643
|
+
return newRequest;
|
|
644
|
+
});
|
|
645
|
+
this.logger.info('Auto-registered OpenRouter provider and enabled OpenRouter mode');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
this.logger.warn('Failed to auto-register OpenRouter provider', { error: e.message });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Automatically detect and register providers based on environment variables
|
|
654
|
+
* This ensures "Zero-Config" initialization works even if createRouter() isn't used.
|
|
655
|
+
* When the first run did not register OpenRouter (e.g. key was missing), a later invoke
|
|
656
|
+
* can still register it by retrying OpenRouter-only registration when the key is now available.
|
|
657
|
+
*/
|
|
658
|
+
async ensureProvidersRegistered() {
|
|
659
|
+
if (this.autoRegistrationAttempted) {
|
|
660
|
+
if (this.providerRegistry.has('openrouter'))
|
|
661
|
+
return;
|
|
662
|
+
// Retry OpenRouter only: key may be available now (e.g. from this.config passed by gateway)
|
|
663
|
+
const detectionConfig = { providers: { openrouter: { apiKey: 'ENV.OPEN_ROUTER_KEY' } } };
|
|
664
|
+
const resolved = resolveEnvPlaceholders(detectionConfig);
|
|
665
|
+
await this.tryRegisterOpenRouter(resolved.providers);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
this.autoRegistrationAttempted = true;
|
|
669
|
+
// Use our own resolver instead of nx-config2 to avoid ESM/CommonJS compatibility issues
|
|
670
|
+
const detectionConfig = {
|
|
671
|
+
providers: {
|
|
672
|
+
openai: {
|
|
673
|
+
apiKey: 'ENV.OPENAI_API_KEY',
|
|
674
|
+
baseURL: 'ENV.OPENAI_API_BASE',
|
|
675
|
+
},
|
|
676
|
+
grok: {
|
|
677
|
+
apiKey: 'ENV.GROK_API_KEY',
|
|
678
|
+
baseURL: 'ENV.XAI_API_BASE',
|
|
679
|
+
},
|
|
680
|
+
openrouter: {
|
|
681
|
+
apiKey: 'ENV.OPEN_ROUTER_KEY',
|
|
682
|
+
},
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
const resolvedConfig = resolveEnvPlaceholders(detectionConfig);
|
|
686
|
+
const config = resolvedConfig.providers;
|
|
687
|
+
await this.tryRegisterOpenRouter(config);
|
|
688
|
+
// If OpenRouter is being used, we SKIP auto-registering other providers
|
|
689
|
+
// to avoid global state conflicts (e.g. ai-provider-openai singleton config).
|
|
690
|
+
if (this.providerRegistry.has('openrouter')) {
|
|
691
|
+
this.logger.info('OpenRouter mode active - skipping individual providers to avoid state conflicts');
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
// 2. OpenAI
|
|
695
|
+
const openaiKey = config.openai?.apiKey;
|
|
696
|
+
const hasOpenaiKey = openaiKey &&
|
|
697
|
+
typeof openaiKey === 'string' &&
|
|
698
|
+
openaiKey.trim() !== '' &&
|
|
699
|
+
!openaiKey.startsWith('ENV.');
|
|
700
|
+
if (hasOpenaiKey && !this.providerRegistry.has('openai')) {
|
|
701
|
+
try {
|
|
702
|
+
const openaiModule = await import('@x12i/ai-provider-openai');
|
|
703
|
+
const openaiConfig = {
|
|
704
|
+
apiKey: openaiKey,
|
|
705
|
+
baseURL: config.openai?.baseURL && !config.openai.baseURL.startsWith('ENV.') ? config.openai.baseURL : undefined,
|
|
706
|
+
timeoutMs: this.config.timeoutMs ?? 60000,
|
|
707
|
+
};
|
|
708
|
+
this.configureProvider('openai', openaiConfig);
|
|
709
|
+
if (openaiModule.initializeClient && typeof openaiModule.initializeClient === 'function') {
|
|
710
|
+
this.registerProvider(openaiModule, 'initializeClient');
|
|
711
|
+
}
|
|
712
|
+
else if (openaiModule.default && typeof openaiModule.default === 'object' && typeof openaiModule.default.initializeClient === 'function') {
|
|
713
|
+
this.registerProvider(openaiModule.default, 'initializeClient');
|
|
714
|
+
}
|
|
715
|
+
this.logger.info('Auto-registered OpenAI provider');
|
|
716
|
+
}
|
|
717
|
+
catch (e) {
|
|
718
|
+
this.logger.warn('Failed to auto-register OpenAI provider', { error: e.message });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// 3. Grok
|
|
722
|
+
const grokKey = config.grok?.apiKey;
|
|
723
|
+
const hasGrokKey = grokKey &&
|
|
724
|
+
typeof grokKey === 'string' &&
|
|
725
|
+
grokKey.trim() !== '' &&
|
|
726
|
+
!grokKey.startsWith('ENV.');
|
|
727
|
+
if (hasGrokKey && !this.providerRegistry.has('grok')) {
|
|
728
|
+
try {
|
|
729
|
+
process.env.XAI_API_KEY = grokKey; // Grok provider often reads from env
|
|
730
|
+
const grokModule = await import('@x12i/ai-provider-grok');
|
|
731
|
+
const grokConfig = {
|
|
732
|
+
apiKey: grokKey,
|
|
733
|
+
baseURL: config.grok?.baseURL && !config.grok.baseURL.startsWith('ENV.') ? config.grok.baseURL : undefined,
|
|
734
|
+
};
|
|
735
|
+
this.configureProvider('grok', grokConfig);
|
|
736
|
+
this.registerProvider(grokModule);
|
|
737
|
+
this.logger.info('Auto-registered Grok provider');
|
|
738
|
+
}
|
|
739
|
+
catch (e) {
|
|
740
|
+
this.logger.warn('Failed to auto-register Grok provider', { error: e.message });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|