@juspay/neurolink 2.0.0 → 3.0.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 (50) hide show
  1. package/CHANGELOG.md +34 -7
  2. package/README.md +38 -34
  3. package/dist/cli/commands/config.d.ts +6 -6
  4. package/dist/cli/index.js +46 -35
  5. package/dist/core/types.d.ts +2 -0
  6. package/dist/lib/core/types.d.ts +2 -0
  7. package/dist/lib/mcp/plugins/filesystem-mcp.d.ts +1 -1
  8. package/dist/lib/neurolink.d.ts +2 -0
  9. package/dist/lib/neurolink.js +23 -2
  10. package/dist/lib/providers/agent-enhanced-provider.d.ts +1 -0
  11. package/dist/lib/providers/agent-enhanced-provider.js +115 -51
  12. package/dist/lib/providers/amazonBedrock.js +74 -24
  13. package/dist/lib/providers/anthropic.js +80 -16
  14. package/dist/lib/providers/azureOpenAI.js +77 -15
  15. package/dist/lib/providers/googleAIStudio.js +77 -26
  16. package/dist/lib/providers/googleVertexAI.js +77 -24
  17. package/dist/lib/providers/huggingFace.js +74 -26
  18. package/dist/lib/providers/mistralAI.js +74 -26
  19. package/dist/lib/providers/ollama.d.ts +1 -1
  20. package/dist/lib/providers/ollama.js +32 -10
  21. package/dist/lib/providers/openAI.js +71 -23
  22. package/dist/lib/providers/timeout-wrapper.d.ts +40 -0
  23. package/dist/lib/providers/timeout-wrapper.js +100 -0
  24. package/dist/lib/proxy/proxy-fetch.d.ts +18 -0
  25. package/dist/lib/proxy/proxy-fetch.js +64 -0
  26. package/dist/lib/utils/timeout.d.ts +69 -0
  27. package/dist/lib/utils/timeout.js +138 -0
  28. package/dist/mcp/plugins/filesystem-mcp.d.ts +1 -1
  29. package/dist/mcp/plugins/filesystem-mcp.js +1 -1
  30. package/dist/neurolink.d.ts +2 -0
  31. package/dist/neurolink.js +23 -2
  32. package/dist/providers/agent-enhanced-provider.d.ts +1 -0
  33. package/dist/providers/agent-enhanced-provider.js +115 -51
  34. package/dist/providers/amazonBedrock.js +74 -24
  35. package/dist/providers/anthropic.js +80 -16
  36. package/dist/providers/azureOpenAI.js +77 -15
  37. package/dist/providers/googleAIStudio.js +77 -26
  38. package/dist/providers/googleVertexAI.js +77 -24
  39. package/dist/providers/huggingFace.js +74 -26
  40. package/dist/providers/mistralAI.js +74 -26
  41. package/dist/providers/ollama.d.ts +1 -1
  42. package/dist/providers/ollama.js +32 -10
  43. package/dist/providers/openAI.js +71 -23
  44. package/dist/providers/timeout-wrapper.d.ts +40 -0
  45. package/dist/providers/timeout-wrapper.js +100 -0
  46. package/dist/proxy/proxy-fetch.d.ts +18 -0
  47. package/dist/proxy/proxy-fetch.js +64 -0
  48. package/dist/utils/timeout.d.ts +69 -0
  49. package/dist/utils/timeout.js +138 -0
  50. package/package.json +2 -1
@@ -9,6 +9,7 @@ import { anthropic } from "@ai-sdk/anthropic";
9
9
  import { getToolsForCategory, } from "../agent/direct-tools.js";
10
10
  import { UnifiedMCPSystem } from "../mcp/unified-mcp.js";
11
11
  import { mcpLogger } from "../mcp/logging.js";
12
+ import { parseTimeout } from "../utils/timeout.js";
12
13
  /**
13
14
  * Agent-Enhanced Provider Class
14
15
  * Provides AI generation with tool calling capabilities
@@ -65,14 +66,15 @@ export class AgentEnhancedProvider {
65
66
  try {
66
67
  mcpLogger.info("[AgentEnhancedProvider] Initializing MCP integration...");
67
68
  this.mcpSystem = new UnifiedMCPSystem({
68
- configPath: this.config.mcpDiscoveryOptions?.configFiles?.[0] || ".mcp-config.json",
69
+ configPath: this.config.mcpDiscoveryOptions?.configFiles?.[0] ||
70
+ ".mcp-config.json",
69
71
  enableExternalServers: true,
70
72
  enableInternalServers: true,
71
- autoInitialize: false
73
+ autoInitialize: false,
72
74
  });
73
75
  // ADD TIMEOUT to prevent hanging forever
74
76
  const initPromise = this.mcpSystem.initialize();
75
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('MCP initialization timeout after 15 seconds')), this.config.mcpInitTimeoutMs || 15000));
77
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("MCP initialization timeout after 15 seconds")), this.config.mcpInitTimeoutMs || 15000));
76
78
  await Promise.race([initPromise, timeoutPromise]);
77
79
  this.mcpInitialized = true;
78
80
  mcpLogger.info("[AgentEnhancedProvider] MCP integration initialized successfully");
@@ -100,10 +102,13 @@ export class AgentEnhancedProvider {
100
102
  return directTools;
101
103
  }
102
104
  // Get MCP tools if available
103
- let mcpTools = {};
105
+ const mcpTools = {};
104
106
  try {
105
107
  // Skip if MCP failed to initialize or is still initializing
106
- if (this.mcpInitFailed || this.mcpInitializing || !this.mcpInitialized || !this.mcpSystem) {
108
+ if (this.mcpInitFailed ||
109
+ this.mcpInitializing ||
110
+ !this.mcpInitialized ||
111
+ !this.mcpSystem) {
107
112
  return directTools;
108
113
  }
109
114
  const mcpToolInfos = await this.mcpSystem.listTools();
@@ -114,33 +119,51 @@ export class AgentEnhancedProvider {
114
119
  description: toolInfo.description || `MCP tool: ${toolInfo.name}`,
115
120
  parameters: toolInfo.inputSchema || {},
116
121
  execute: async (args) => {
122
+ let timeoutId;
117
123
  try {
124
+ // Create timeout controller for tool execution if configured
125
+ const toolTimeout = this.config.toolExecutionTimeout;
126
+ const toolAbortController = toolTimeout
127
+ ? new AbortController()
128
+ : undefined;
129
+ if (toolAbortController && toolTimeout) {
130
+ const timeoutMs = typeof toolTimeout === "string"
131
+ ? parseTimeout(toolTimeout)
132
+ : toolTimeout;
133
+ timeoutId = setTimeout(() => {
134
+ toolAbortController.abort();
135
+ }, timeoutMs);
136
+ }
118
137
  const context = {
119
- sessionId: 'cli-session',
120
- userId: 'cli-user',
138
+ sessionId: "cli-session",
139
+ userId: "cli-user",
121
140
  secureFS: {
122
141
  readFile: async (path, encoding) => {
123
- const fs = await import('fs/promises');
124
- return encoding ? fs.readFile(path, { encoding: encoding }) : fs.readFile(path);
142
+ const fs = await import("fs/promises");
143
+ return encoding
144
+ ? fs.readFile(path, {
145
+ encoding: encoding,
146
+ })
147
+ : fs.readFile(path);
125
148
  },
126
149
  writeFile: async (path, content) => {
127
- const fs = await import('fs/promises');
150
+ const fs = await import("fs/promises");
128
151
  await fs.writeFile(path, content);
129
152
  },
130
153
  readdir: async (path) => {
131
- const fs = await import('fs/promises');
154
+ const fs = await import("fs/promises");
132
155
  return fs.readdir(path);
133
156
  },
134
157
  stat: async (path) => {
135
- const fs = await import('fs/promises');
158
+ const fs = await import("fs/promises");
136
159
  return fs.stat(path);
137
160
  },
138
161
  mkdir: async (path, options) => {
139
- const fs = await import('fs/promises');
162
+ const fs = await import("fs/promises");
140
163
  await fs.mkdir(path, options);
141
164
  },
142
165
  exists: async (path) => {
143
- const fs = await import('fs/promises');
166
+ const fs = await import("fs/promises");
144
167
  try {
145
168
  await fs.access(path);
146
169
  return true;
@@ -148,34 +171,34 @@ export class AgentEnhancedProvider {
148
171
  catch {
149
172
  return false;
150
173
  }
151
- }
174
+ },
152
175
  },
153
176
  path: {
154
177
  join: (...paths) => {
155
- const path = require('path');
178
+ const path = require("path");
156
179
  return path.join(...paths);
157
180
  },
158
181
  resolve: (...paths) => {
159
- const path = require('path');
182
+ const path = require("path");
160
183
  return path.resolve(...paths);
161
184
  },
162
185
  relative: (from, to) => {
163
- const path = require('path');
186
+ const path = require("path");
164
187
  return path.relative(from, to);
165
188
  },
166
189
  dirname: (path) => {
167
- const pathLib = require('path');
190
+ const pathLib = require("path");
168
191
  return pathLib.dirname(path);
169
192
  },
170
193
  basename: (path, ext) => {
171
- const pathLib = require('path');
194
+ const pathLib = require("path");
172
195
  return pathLib.basename(path, ext);
173
- }
196
+ },
174
197
  },
175
- grantedPermissions: ['read', 'write', 'execute'],
198
+ grantedPermissions: ["read", "write", "execute"],
176
199
  log: (level, message, data) => {
177
200
  const logFn = mcpLogger[level];
178
- if (typeof logFn === 'function') {
201
+ if (typeof logFn === "function") {
179
202
  if (data) {
180
203
  logFn(`${message} ${JSON.stringify(data)}`);
181
204
  }
@@ -183,16 +206,39 @@ export class AgentEnhancedProvider {
183
206
  logFn(message);
184
207
  }
185
208
  }
186
- }
209
+ },
187
210
  };
188
- const result = await this.mcpSystem.executeTool(toolInfo.name, args, context);
211
+ const toolPromise = this.mcpSystem.executeTool(toolInfo.name, args, context);
212
+ let result;
213
+ if (toolAbortController) {
214
+ // Race between tool execution and timeout
215
+ result = await Promise.race([
216
+ toolPromise,
217
+ new Promise((_, reject) => {
218
+ toolAbortController.signal.addEventListener("abort", () => {
219
+ reject(new Error(`Tool ${toolInfo.name} timed out after ${this.config.toolExecutionTimeout}`));
220
+ });
221
+ }),
222
+ ]);
223
+ }
224
+ else {
225
+ result = await toolPromise;
226
+ }
227
+ // Clear timeout if successful
228
+ if (timeoutId) {
229
+ clearTimeout(timeoutId);
230
+ }
189
231
  return result.data || result;
190
232
  }
191
233
  catch (error) {
234
+ // Clear timeout on error
235
+ if (timeoutId) {
236
+ clearTimeout(timeoutId);
237
+ }
192
238
  mcpLogger.error(`MCP tool ${toolInfo.name} execution failed:`, error);
193
239
  throw error;
194
240
  }
195
- }
241
+ },
196
242
  };
197
243
  }
198
244
  mcpLogger.info(`[AgentEnhancedProvider] Loaded ${Object.keys(mcpTools).length} MCP tools`);
@@ -206,20 +252,26 @@ export class AgentEnhancedProvider {
206
252
  const options = typeof optionsOrPrompt === "string"
207
253
  ? { prompt: optionsOrPrompt }
208
254
  : optionsOrPrompt;
209
- const { prompt, temperature = 0.7, maxTokens = 1000, systemPrompt, schema, } = options;
255
+ const { prompt, temperature = 0.7, maxTokens = 1000, systemPrompt, schema, timeout, } = options;
210
256
  // Get combined tools (direct + MCP) if enabled
211
- const tools = this.config.enableTools
212
- ? await this.getCombinedTools()
213
- : {};
257
+ const tools = this.config.enableTools ? await this.getCombinedTools() : {};
214
258
  const log = (msg, data) => {
215
- mcpLogger.info(`[AgentEnhancedProvider] ${msg}`, data ? JSON.stringify(data, null, 2) : '');
259
+ mcpLogger.info(`[AgentEnhancedProvider] ${msg}`, data ? JSON.stringify(data, null, 2) : "");
216
260
  };
217
- log('Starting text generation', {
261
+ log("Starting text generation", {
218
262
  prompt: prompt.substring(0, 100),
219
263
  toolsCount: Object.keys(tools).length,
220
- maxSteps: this.config.maxSteps
264
+ maxSteps: this.config.maxSteps,
221
265
  });
222
266
  try {
267
+ // Parse timeout if provided
268
+ let abortSignal;
269
+ if (timeout) {
270
+ const timeoutMs = typeof timeout === "string" ? parseTimeout(timeout) : timeout;
271
+ if (timeoutMs !== undefined) {
272
+ abortSignal = AbortSignal.timeout(timeoutMs);
273
+ }
274
+ }
223
275
  // The AI SDK with maxSteps automatically handles tool calling and result integration
224
276
  const result = await generateText({
225
277
  model: this.model,
@@ -231,25 +283,28 @@ export class AgentEnhancedProvider {
231
283
  temperature,
232
284
  maxTokens,
233
285
  toolChoice: this.shouldForceToolUsage(prompt) ? "required" : "auto",
286
+ abortSignal, // Pass abort signal for timeout support
234
287
  });
235
- log('Generation completed', {
288
+ log("Generation completed", {
236
289
  text: result.text?.substring(0, 200),
237
290
  finishReason: result.finishReason,
238
291
  toolCallsCount: result.toolCalls?.length || 0,
239
292
  toolResultsCount: result.toolResults?.length || 0,
240
- stepsCount: result.steps?.length || 0
293
+ stepsCount: result.steps?.length || 0,
241
294
  });
242
295
  // Check if tools were called but no final text was generated
243
- if (result.finishReason === 'tool-calls' && !result.text && result.toolResults?.length > 0) {
244
- log('Tools called but no final text generated, creating summary response');
296
+ if (result.finishReason === "tool-calls" &&
297
+ !result.text &&
298
+ result.toolResults?.length > 0) {
299
+ log("Tools called but no final text generated, creating summary response");
245
300
  try {
246
301
  // Extract tool results and create a summary prompt
247
- let toolResultsSummary = '';
302
+ let toolResultsSummary = "";
248
303
  if (result.toolResults) {
249
304
  for (const toolResult of result.toolResults) {
250
305
  const resultData = toolResult.result || toolResult;
251
306
  // Try to extract meaningful data from the result
252
- if (typeof resultData === 'object' && resultData !== null) {
307
+ if (typeof resultData === "object" && resultData !== null) {
253
308
  if (resultData.success && resultData.items) {
254
309
  // This looks like a filesystem listing
255
310
  toolResultsSummary += `Directory listing for ${resultData.path}:\n`;
@@ -264,34 +319,36 @@ export class AgentEnhancedProvider {
264
319
  else {
265
320
  toolResultsSummary += String(resultData);
266
321
  }
267
- toolResultsSummary += '\n\n';
322
+ toolResultsSummary += "\n\n";
268
323
  }
269
324
  }
270
- log('Tool results extracted', {
325
+ log("Tool results extracted", {
271
326
  summaryLength: toolResultsSummary.length,
272
- preview: toolResultsSummary.substring(0, 200)
327
+ preview: toolResultsSummary.substring(0, 200),
273
328
  });
274
329
  // Create a simple, direct summary
275
330
  const finalText = `Based on the user request "${prompt}", here's what I found:\n\n${toolResultsSummary}`;
276
- log('Final text created', {
331
+ log("Final text created", {
277
332
  textLength: finalText.length,
278
- preview: finalText.substring(0, 200)
333
+ preview: finalText.substring(0, 200),
279
334
  });
280
335
  // Return result with the formatted text
281
336
  return {
282
337
  ...result,
283
338
  text: finalText,
284
- finishReason: 'stop'
339
+ finishReason: "stop",
285
340
  };
286
341
  }
287
342
  catch (error) {
288
- log('Error in summary generation', { error: error instanceof Error ? error.message : String(error) });
343
+ log("Error in summary generation", {
344
+ error: error instanceof Error ? error.message : String(error),
345
+ });
289
346
  // Fallback: return raw tool results
290
347
  const fallbackText = `Tool execution completed. Raw results: ${JSON.stringify(result.toolResults, null, 2)}`;
291
348
  return {
292
349
  ...result,
293
350
  text: fallbackText,
294
- finishReason: 'stop'
351
+ finishReason: "stop",
295
352
  };
296
353
  }
297
354
  }
@@ -307,12 +364,18 @@ export class AgentEnhancedProvider {
307
364
  const options = typeof optionsOrPrompt === "string"
308
365
  ? { prompt: optionsOrPrompt }
309
366
  : optionsOrPrompt;
310
- const { prompt, temperature = 0.7, maxTokens = 1000, systemPrompt, } = options;
367
+ const { prompt, temperature = 0.7, maxTokens = 1000, systemPrompt, timeout, } = options;
311
368
  // Get combined tools (direct + MCP) if enabled
312
- const tools = this.config.enableTools
313
- ? await this.getCombinedTools()
314
- : {};
369
+ const tools = this.config.enableTools ? await this.getCombinedTools() : {};
315
370
  try {
371
+ // Parse timeout if provided
372
+ let abortSignal;
373
+ if (timeout) {
374
+ const timeoutMs = typeof timeout === "string" ? parseTimeout(timeout) : timeout;
375
+ if (timeoutMs !== undefined) {
376
+ abortSignal = AbortSignal.timeout(timeoutMs);
377
+ }
378
+ }
316
379
  const result = await streamText({
317
380
  model: this.model,
318
381
  prompt: systemPrompt
@@ -323,6 +386,7 @@ export class AgentEnhancedProvider {
323
386
  temperature,
324
387
  maxTokens,
325
388
  toolChoice: this.shouldForceToolUsage(prompt) ? "required" : "auto",
389
+ abortSignal, // Pass abort signal for timeout support
326
390
  });
327
391
  return result;
328
392
  }
@@ -1,6 +1,7 @@
1
1
  import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
2
2
  import { streamText, generateText, Output, } from "ai";
3
3
  import { logger } from "../utils/logger.js";
4
+ import { createTimeoutController, TimeoutError, getDefaultTimeout, } from "../utils/timeout.js";
4
5
  // Default system context
5
6
  const DEFAULT_SYSTEM_CONTEXT = {
6
7
  systemPrompt: "You are a helpful AI assistant.",
@@ -128,7 +129,7 @@ export class AmazonBedrock {
128
129
  const options = typeof optionsOrPrompt === "string"
129
130
  ? { prompt: optionsOrPrompt }
130
131
  : optionsOrPrompt;
131
- const { prompt, temperature = 0.7, maxTokens = 1000, systemPrompt = DEFAULT_SYSTEM_CONTEXT.systemPrompt, schema, } = options;
132
+ const { prompt, temperature = 0.7, maxTokens = 1000, systemPrompt = DEFAULT_SYSTEM_CONTEXT.systemPrompt, schema, timeout = getDefaultTimeout(provider, "stream"), } = options;
132
133
  // Use schema from options or fallback parameter
133
134
  const finalSchema = schema || analysisSchema;
134
135
  logger.debug(`[${functionTag}] Stream request started`, {
@@ -137,13 +138,20 @@ export class AmazonBedrock {
137
138
  promptLength: prompt.length,
138
139
  temperature,
139
140
  maxTokens,
141
+ timeout,
140
142
  });
143
+ // Create timeout controller if timeout is specified
144
+ const timeoutController = createTimeoutController(timeout, provider, "stream");
141
145
  const streamOptions = {
142
146
  model: this.model,
143
147
  prompt: prompt,
144
148
  system: systemPrompt,
145
149
  temperature,
146
150
  maxTokens,
151
+ // Add abort signal if available
152
+ ...(timeoutController && {
153
+ abortSignal: timeoutController.controller.signal,
154
+ }),
147
155
  onError: (event) => {
148
156
  const error = event.error;
149
157
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -193,16 +201,30 @@ export class AmazonBedrock {
193
201
  modelName: this.modelName,
194
202
  promptLength: prompt.length,
195
203
  });
204
+ // For streaming, we can't clean up immediately, but the timeout will auto-clean
205
+ // The user should handle the stream and any timeout errors
196
206
  return result;
197
207
  }
198
208
  catch (err) {
199
- logger.error(`[${functionTag}] Exception`, {
200
- provider,
201
- modelName: this.modelName,
202
- region: getAWSRegion(),
203
- message: "Error in streaming text",
204
- err: String(err),
205
- });
209
+ // Log timeout errors specifically
210
+ if (err instanceof TimeoutError) {
211
+ logger.error(`[${functionTag}] Timeout error`, {
212
+ provider,
213
+ modelName: this.modelName,
214
+ region: getAWSRegion(),
215
+ timeout: err.timeout,
216
+ message: err.message,
217
+ });
218
+ }
219
+ else {
220
+ logger.error(`[${functionTag}] Exception`, {
221
+ provider,
222
+ modelName: this.modelName,
223
+ region: getAWSRegion(),
224
+ message: "Error in streaming text",
225
+ err: String(err),
226
+ });
227
+ }
206
228
  throw err; // Re-throw error to trigger fallback
207
229
  }
208
230
  }
@@ -214,7 +236,7 @@ export class AmazonBedrock {
214
236
  const options = typeof optionsOrPrompt === "string"
215
237
  ? { prompt: optionsOrPrompt }
216
238
  : optionsOrPrompt;
217
- const { prompt, temperature = 0.7, maxTokens = 1000, systemPrompt = DEFAULT_SYSTEM_CONTEXT.systemPrompt, schema, } = options;
239
+ const { prompt, temperature = 0.7, maxTokens = 1000, systemPrompt = DEFAULT_SYSTEM_CONTEXT.systemPrompt, schema, timeout = getDefaultTimeout(provider, "generate"), } = options;
218
240
  // Use schema from options or fallback parameter
219
241
  const finalSchema = schema || analysisSchema;
220
242
  logger.debug(`[${functionTag}] Generate text started`, {
@@ -224,36 +246,64 @@ export class AmazonBedrock {
224
246
  promptLength: prompt.length,
225
247
  temperature,
226
248
  maxTokens,
249
+ timeout,
227
250
  });
251
+ // Create timeout controller if timeout is specified
252
+ const timeoutController = createTimeoutController(timeout, provider, "generate");
228
253
  const generateOptions = {
229
254
  model: this.model,
230
255
  prompt: prompt,
231
256
  system: systemPrompt,
232
257
  temperature,
233
258
  maxTokens,
259
+ // Add abort signal if available
260
+ ...(timeoutController && {
261
+ abortSignal: timeoutController.controller.signal,
262
+ }),
234
263
  };
235
264
  if (finalSchema) {
236
265
  generateOptions.experimental_output = Output.object({
237
266
  schema: finalSchema,
238
267
  });
239
268
  }
240
- const result = await generateText(generateOptions);
241
- logger.debug(`[${functionTag}] Generate text completed`, {
242
- provider,
243
- modelName: this.modelName,
244
- usage: result.usage,
245
- finishReason: result.finishReason,
246
- responseLength: result.text?.length || 0,
247
- });
248
- return result;
269
+ try {
270
+ const result = await generateText(generateOptions);
271
+ // Clean up timeout if successful
272
+ timeoutController?.cleanup();
273
+ logger.debug(`[${functionTag}] Generate text completed`, {
274
+ provider,
275
+ modelName: this.modelName,
276
+ usage: result.usage,
277
+ finishReason: result.finishReason,
278
+ responseLength: result.text?.length || 0,
279
+ timeout,
280
+ });
281
+ return result;
282
+ }
283
+ finally {
284
+ // Always cleanup timeout
285
+ timeoutController?.cleanup();
286
+ }
249
287
  }
250
288
  catch (err) {
251
- logger.error(`[${functionTag}] Exception`, {
252
- provider,
253
- modelName: this.modelName,
254
- message: "Error in generating text",
255
- err: String(err),
256
- });
289
+ // Log timeout errors specifically
290
+ if (err instanceof TimeoutError) {
291
+ logger.error(`[${functionTag}] Timeout error`, {
292
+ provider,
293
+ modelName: this.modelName,
294
+ region: getAWSRegion(),
295
+ timeout: err.timeout,
296
+ message: err.message,
297
+ });
298
+ }
299
+ else {
300
+ logger.error(`[${functionTag}] Exception`, {
301
+ provider,
302
+ modelName: this.modelName,
303
+ message: "Error in generating text",
304
+ err: String(err),
305
+ });
306
+ }
257
307
  throw err; // Re-throw error to trigger fallback instead of returning null
258
308
  }
259
309
  }