@x12i/ai-providers-router 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.metadata/anthropic.response-map.json +1 -0
  2. package/.metadata/google.response-map.json +1 -0
  3. package/.metadata/groq.response-map.json +1 -0
  4. package/.metadata/llm-request-config-registry.json +111 -0
  5. package/.metadata/llm-response-config-registry.json +1 -0
  6. package/.metadata/model-aliases.json +1 -0
  7. package/.metadata/model-normalization.json +1 -0
  8. package/.metadata/moonshot.response-map.json +1 -0
  9. package/.metadata/openai.response-map.json +1 -0
  10. package/.metadata/openrouter_catalog_with_vendor_mapping.json +15781 -0
  11. package/.metadata/reasoning-support.json +159 -0
  12. package/.metadata/xai.response-map.json +1 -0
  13. package/README.md +480 -0
  14. package/dist/adapters/grok/GrokAdapter.d.ts +50 -0
  15. package/dist/adapters/grok/GrokAdapter.js +342 -0
  16. package/dist/adapters/openai/OpenAIAdapter.d.ts +50 -0
  17. package/dist/adapters/openai/OpenAIAdapter.js +401 -0
  18. package/dist/adapters/openrouter/OpenRouterAdapter.d.ts +87 -0
  19. package/dist/adapters/openrouter/OpenRouterAdapter.js +1449 -0
  20. package/dist/adapters/openrouter/reasoning-capabilities.d.ts +26 -0
  21. package/dist/adapters/openrouter/reasoning-capabilities.js +79 -0
  22. package/dist/discovery.d.ts +6 -0
  23. package/dist/discovery.js +114 -0
  24. package/dist/errors.d.ts +27 -0
  25. package/dist/errors.js +33 -0
  26. package/dist/factory.d.ts +15 -0
  27. package/dist/factory.js +206 -0
  28. package/dist/gateway.d.ts +22 -0
  29. package/dist/gateway.js +154 -0
  30. package/dist/index.d.ts +9 -0
  31. package/dist/index.js +42 -0
  32. package/dist/interceptors.d.ts +10 -0
  33. package/dist/interceptors.js +1 -0
  34. package/dist/logger.d.ts +70 -0
  35. package/dist/logger.js +222 -0
  36. package/dist/openrouter-catalog.d.ts +119 -0
  37. package/dist/openrouter-catalog.js +222 -0
  38. package/dist/providers/OpenRouterProvider.d.ts +16 -0
  39. package/dist/providers/OpenRouterProvider.js +171 -0
  40. package/dist/registry/AdapterRegistry.d.ts +86 -0
  41. package/dist/registry/AdapterRegistry.js +36 -0
  42. package/dist/registry/ProviderRegistry.d.ts +24 -0
  43. package/dist/registry/ProviderRegistry.js +46 -0
  44. package/dist/router/Router.d.ts +33 -0
  45. package/dist/router/Router.js +258 -0
  46. package/dist/router/RouterTypes.d.ts +138 -0
  47. package/dist/router/RouterTypes.js +5 -0
  48. package/dist/router/RouterWrapper.d.ts +83 -0
  49. package/dist/router/RouterWrapper.js +744 -0
  50. package/dist/router.d.ts +13 -0
  51. package/dist/router.js +8 -0
  52. package/dist/types.d.ts +33 -0
  53. package/dist/types.js +1 -0
  54. package/dist/utils/esm-compat.d.ts +9 -0
  55. package/dist/utils/esm-compat.js +13 -0
  56. package/dist/utils/ids.d.ts +4 -0
  57. package/dist/utils/ids.js +6 -0
  58. package/package.json +66 -0
@@ -0,0 +1,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
+ }