@juspay/neurolink 8.35.0 → 8.35.2
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/CHANGELOG.md +12 -0
- package/dist/lib/types/fileTypes.d.ts +4 -0
- package/dist/lib/utils/fileDetector.d.ts +1 -1
- package/dist/lib/utils/fileDetector.js +133 -19
- package/dist/lib/utils/pdfProcessor.js +4 -0
- package/dist/types/fileTypes.d.ts +4 -0
- package/dist/utils/fileDetector.d.ts +1 -1
- package/dist/utils/fileDetector.js +133 -19
- package/dist/utils/pdfProcessor.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [8.35.2](https://github.com/juspay/neurolink/compare/v8.35.1...v8.35.2) (2026-01-15)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- **(provider):** add network retry logic with exponential backoff to detection operations ([3b29e24](https://github.com/juspay/neurolink/commit/3b29e248766fac01f1e3ea368e9439233957121e))
|
|
6
|
+
|
|
7
|
+
## [8.35.1](https://github.com/juspay/neurolink/compare/v8.35.0...v8.35.1) (2026-01-15)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **(pdf):** reject empty PDFs with 0 pages instead of returning success ([92d8d4e](https://github.com/juspay/neurolink/commit/92d8d4eb3e6e675ee969e3a8ee92c5997dc416aa))
|
|
12
|
+
|
|
1
13
|
## [8.35.0](https://github.com/juspay/neurolink/compare/v8.34.1...v8.35.0) (2026-01-15)
|
|
2
14
|
|
|
3
15
|
### Features
|
|
@@ -225,6 +225,10 @@ export type FileDetectorOptions = {
|
|
|
225
225
|
officeOptions?: OfficeProcessorOptions;
|
|
226
226
|
confidenceThreshold?: number;
|
|
227
227
|
provider?: string;
|
|
228
|
+
/** Maximum number of retry attempts for network requests (default: 3) */
|
|
229
|
+
maxRetries?: number;
|
|
230
|
+
/** Initial retry delay in milliseconds with exponential backoff (default: 1000) */
|
|
231
|
+
retryDelay?: number;
|
|
228
232
|
};
|
|
229
233
|
/**
|
|
230
234
|
* Google AI Studio Files API types
|
|
@@ -9,6 +9,116 @@ import { logger } from "./logger.js";
|
|
|
9
9
|
import { CSVProcessor } from "./csvProcessor.js";
|
|
10
10
|
import { ImageProcessor } from "./imageProcessor.js";
|
|
11
11
|
import { PDFProcessor } from "./pdfProcessor.js";
|
|
12
|
+
/**
|
|
13
|
+
* Default retry configuration constants
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
16
|
+
const DEFAULT_RETRY_DELAY = 1000; // milliseconds
|
|
17
|
+
/**
|
|
18
|
+
* Retryable network error codes (Node.js/undici network errors)
|
|
19
|
+
*/
|
|
20
|
+
const RETRYABLE_ERROR_CODES = [
|
|
21
|
+
"ETIMEDOUT",
|
|
22
|
+
"ECONNRESET",
|
|
23
|
+
"ECONNREFUSED",
|
|
24
|
+
"ENOTFOUND",
|
|
25
|
+
"ENETUNREACH",
|
|
26
|
+
"EAI_AGAIN",
|
|
27
|
+
"EPIPE",
|
|
28
|
+
"ECONNABORTED",
|
|
29
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
30
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
31
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
32
|
+
"UND_ERR_SOCKET",
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Non-retryable HTTP status codes (client errors)
|
|
36
|
+
*/
|
|
37
|
+
const NON_RETRYABLE_STATUS_CODES = [400, 401, 403, 404, 405];
|
|
38
|
+
/**
|
|
39
|
+
* Retryable HTTP status codes (server errors + rate limiting)
|
|
40
|
+
*/
|
|
41
|
+
const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];
|
|
42
|
+
/**
|
|
43
|
+
* Check if an error is a recoverable network error that should be retried
|
|
44
|
+
*
|
|
45
|
+
* @param error - Error to check
|
|
46
|
+
* @returns True if error is retryable (transient network issue)
|
|
47
|
+
*/
|
|
48
|
+
function isRetryableNetworkError(error) {
|
|
49
|
+
if (!(error instanceof Error)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const errorMessage = error.message.toLowerCase();
|
|
53
|
+
// Extract error code from various error shapes
|
|
54
|
+
const errorWithCode = error;
|
|
55
|
+
const errorCode = errorWithCode.code?.toUpperCase();
|
|
56
|
+
// Check for retryable network error codes
|
|
57
|
+
if (errorCode && RETRYABLE_ERROR_CODES.includes(errorCode)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// Check HTTP status code if present in error message (e.g., "HTTP 503")
|
|
61
|
+
const httpStatusMatch = errorMessage.match(/http\s*(\d{3})/);
|
|
62
|
+
if (httpStatusMatch) {
|
|
63
|
+
const statusCode = parseInt(httpStatusMatch[1], 10);
|
|
64
|
+
if (NON_RETRYABLE_STATUS_CODES.includes(statusCode)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (RETRYABLE_STATUS_CODES.includes(statusCode)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Check error message for transient issues
|
|
72
|
+
const transientKeywords = [
|
|
73
|
+
"timeout",
|
|
74
|
+
"timed out",
|
|
75
|
+
"connection reset",
|
|
76
|
+
"econnreset",
|
|
77
|
+
"etimedout",
|
|
78
|
+
"network error",
|
|
79
|
+
"socket hang up",
|
|
80
|
+
"enotfound",
|
|
81
|
+
"getaddrinfo",
|
|
82
|
+
"unavailable",
|
|
83
|
+
"service unavailable",
|
|
84
|
+
];
|
|
85
|
+
return transientKeywords.some((keyword) => errorMessage.includes(keyword));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Execute an operation with automatic retry logic on transient network errors
|
|
89
|
+
*
|
|
90
|
+
* @param operation - Async function to execute
|
|
91
|
+
* @param options - Retry configuration options
|
|
92
|
+
* @returns Promise resolving to the operation result
|
|
93
|
+
* @throws Error if all retry attempts fail or error is non-retryable
|
|
94
|
+
*/
|
|
95
|
+
async function withRetry(operation, options = {}) {
|
|
96
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
97
|
+
const retryDelay = options.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
98
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
99
|
+
try {
|
|
100
|
+
return await operation();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const isRetryable = isRetryableNetworkError(error);
|
|
104
|
+
const isLastAttempt = attempt === maxRetries;
|
|
105
|
+
if (!isRetryable || isLastAttempt) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
// Calculate exponential backoff delay
|
|
109
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
110
|
+
logger.debug("Retrying network operation after transient error", {
|
|
111
|
+
attempt: attempt + 1,
|
|
112
|
+
maxRetries,
|
|
113
|
+
delay,
|
|
114
|
+
error: error instanceof Error ? error.message : String(error),
|
|
115
|
+
});
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// TypeScript exhaustiveness check - should never reach here
|
|
120
|
+
throw new Error("Retry logic failed unexpectedly");
|
|
121
|
+
}
|
|
12
122
|
/**
|
|
13
123
|
* Check if text has JSON markers (starts with { or [ and ends with corresponding closing bracket)
|
|
14
124
|
*/
|
|
@@ -356,30 +466,34 @@ export class FileDetector {
|
|
|
356
466
|
}
|
|
357
467
|
}
|
|
358
468
|
/**
|
|
359
|
-
* Load file from URL
|
|
469
|
+
* Load file from URL with automatic retry on transient network errors
|
|
360
470
|
*/
|
|
361
471
|
static async loadFromURL(url, options) {
|
|
362
472
|
const maxSize = options?.maxSize || 10 * 1024 * 1024;
|
|
363
473
|
const timeout = options?.timeout || this.DEFAULT_NETWORK_TIMEOUT;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
for await (const chunk of response.body) {
|
|
376
|
-
totalSize += chunk.length;
|
|
377
|
-
if (totalSize > maxSize) {
|
|
378
|
-
throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
|
|
474
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
475
|
+
const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
476
|
+
return withRetry(async () => {
|
|
477
|
+
const response = await request(url, {
|
|
478
|
+
dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
|
|
479
|
+
method: "GET",
|
|
480
|
+
headersTimeout: timeout,
|
|
481
|
+
bodyTimeout: timeout,
|
|
482
|
+
});
|
|
483
|
+
if (response.statusCode !== 200) {
|
|
484
|
+
throw new Error(`HTTP ${response.statusCode}`);
|
|
379
485
|
}
|
|
380
|
-
chunks
|
|
381
|
-
|
|
382
|
-
|
|
486
|
+
const chunks = [];
|
|
487
|
+
let totalSize = 0;
|
|
488
|
+
for await (const chunk of response.body) {
|
|
489
|
+
totalSize += chunk.length;
|
|
490
|
+
if (totalSize > maxSize) {
|
|
491
|
+
throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
|
|
492
|
+
}
|
|
493
|
+
chunks.push(chunk);
|
|
494
|
+
}
|
|
495
|
+
return Buffer.concat(chunks);
|
|
496
|
+
}, { maxRetries, retryDelay });
|
|
383
497
|
}
|
|
384
498
|
/**
|
|
385
499
|
* Load file from filesystem path
|
|
@@ -309,6 +309,10 @@ export class PDFProcessor {
|
|
|
309
309
|
base64Length: base64Image.length,
|
|
310
310
|
});
|
|
311
311
|
}
|
|
312
|
+
// Check for empty PDF (0 pages)
|
|
313
|
+
if (images.length === 0) {
|
|
314
|
+
throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
|
|
315
|
+
}
|
|
312
316
|
const conversionTimeMs = Date.now() - startTime;
|
|
313
317
|
logger.info("[PDF→Image] ✅ PDF conversion completed", {
|
|
314
318
|
pageCount: images.length,
|
|
@@ -225,6 +225,10 @@ export type FileDetectorOptions = {
|
|
|
225
225
|
officeOptions?: OfficeProcessorOptions;
|
|
226
226
|
confidenceThreshold?: number;
|
|
227
227
|
provider?: string;
|
|
228
|
+
/** Maximum number of retry attempts for network requests (default: 3) */
|
|
229
|
+
maxRetries?: number;
|
|
230
|
+
/** Initial retry delay in milliseconds with exponential backoff (default: 1000) */
|
|
231
|
+
retryDelay?: number;
|
|
228
232
|
};
|
|
229
233
|
/**
|
|
230
234
|
* Google AI Studio Files API types
|
|
@@ -9,6 +9,116 @@ import { logger } from "./logger.js";
|
|
|
9
9
|
import { CSVProcessor } from "./csvProcessor.js";
|
|
10
10
|
import { ImageProcessor } from "./imageProcessor.js";
|
|
11
11
|
import { PDFProcessor } from "./pdfProcessor.js";
|
|
12
|
+
/**
|
|
13
|
+
* Default retry configuration constants
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
16
|
+
const DEFAULT_RETRY_DELAY = 1000; // milliseconds
|
|
17
|
+
/**
|
|
18
|
+
* Retryable network error codes (Node.js/undici network errors)
|
|
19
|
+
*/
|
|
20
|
+
const RETRYABLE_ERROR_CODES = [
|
|
21
|
+
"ETIMEDOUT",
|
|
22
|
+
"ECONNRESET",
|
|
23
|
+
"ECONNREFUSED",
|
|
24
|
+
"ENOTFOUND",
|
|
25
|
+
"ENETUNREACH",
|
|
26
|
+
"EAI_AGAIN",
|
|
27
|
+
"EPIPE",
|
|
28
|
+
"ECONNABORTED",
|
|
29
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
30
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
31
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
32
|
+
"UND_ERR_SOCKET",
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Non-retryable HTTP status codes (client errors)
|
|
36
|
+
*/
|
|
37
|
+
const NON_RETRYABLE_STATUS_CODES = [400, 401, 403, 404, 405];
|
|
38
|
+
/**
|
|
39
|
+
* Retryable HTTP status codes (server errors + rate limiting)
|
|
40
|
+
*/
|
|
41
|
+
const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];
|
|
42
|
+
/**
|
|
43
|
+
* Check if an error is a recoverable network error that should be retried
|
|
44
|
+
*
|
|
45
|
+
* @param error - Error to check
|
|
46
|
+
* @returns True if error is retryable (transient network issue)
|
|
47
|
+
*/
|
|
48
|
+
function isRetryableNetworkError(error) {
|
|
49
|
+
if (!(error instanceof Error)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const errorMessage = error.message.toLowerCase();
|
|
53
|
+
// Extract error code from various error shapes
|
|
54
|
+
const errorWithCode = error;
|
|
55
|
+
const errorCode = errorWithCode.code?.toUpperCase();
|
|
56
|
+
// Check for retryable network error codes
|
|
57
|
+
if (errorCode && RETRYABLE_ERROR_CODES.includes(errorCode)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// Check HTTP status code if present in error message (e.g., "HTTP 503")
|
|
61
|
+
const httpStatusMatch = errorMessage.match(/http\s*(\d{3})/);
|
|
62
|
+
if (httpStatusMatch) {
|
|
63
|
+
const statusCode = parseInt(httpStatusMatch[1], 10);
|
|
64
|
+
if (NON_RETRYABLE_STATUS_CODES.includes(statusCode)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (RETRYABLE_STATUS_CODES.includes(statusCode)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Check error message for transient issues
|
|
72
|
+
const transientKeywords = [
|
|
73
|
+
"timeout",
|
|
74
|
+
"timed out",
|
|
75
|
+
"connection reset",
|
|
76
|
+
"econnreset",
|
|
77
|
+
"etimedout",
|
|
78
|
+
"network error",
|
|
79
|
+
"socket hang up",
|
|
80
|
+
"enotfound",
|
|
81
|
+
"getaddrinfo",
|
|
82
|
+
"unavailable",
|
|
83
|
+
"service unavailable",
|
|
84
|
+
];
|
|
85
|
+
return transientKeywords.some((keyword) => errorMessage.includes(keyword));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Execute an operation with automatic retry logic on transient network errors
|
|
89
|
+
*
|
|
90
|
+
* @param operation - Async function to execute
|
|
91
|
+
* @param options - Retry configuration options
|
|
92
|
+
* @returns Promise resolving to the operation result
|
|
93
|
+
* @throws Error if all retry attempts fail or error is non-retryable
|
|
94
|
+
*/
|
|
95
|
+
async function withRetry(operation, options = {}) {
|
|
96
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
97
|
+
const retryDelay = options.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
98
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
99
|
+
try {
|
|
100
|
+
return await operation();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const isRetryable = isRetryableNetworkError(error);
|
|
104
|
+
const isLastAttempt = attempt === maxRetries;
|
|
105
|
+
if (!isRetryable || isLastAttempt) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
// Calculate exponential backoff delay
|
|
109
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
110
|
+
logger.debug("Retrying network operation after transient error", {
|
|
111
|
+
attempt: attempt + 1,
|
|
112
|
+
maxRetries,
|
|
113
|
+
delay,
|
|
114
|
+
error: error instanceof Error ? error.message : String(error),
|
|
115
|
+
});
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// TypeScript exhaustiveness check - should never reach here
|
|
120
|
+
throw new Error("Retry logic failed unexpectedly");
|
|
121
|
+
}
|
|
12
122
|
/**
|
|
13
123
|
* Check if text has JSON markers (starts with { or [ and ends with corresponding closing bracket)
|
|
14
124
|
*/
|
|
@@ -356,30 +466,34 @@ export class FileDetector {
|
|
|
356
466
|
}
|
|
357
467
|
}
|
|
358
468
|
/**
|
|
359
|
-
* Load file from URL
|
|
469
|
+
* Load file from URL with automatic retry on transient network errors
|
|
360
470
|
*/
|
|
361
471
|
static async loadFromURL(url, options) {
|
|
362
472
|
const maxSize = options?.maxSize || 10 * 1024 * 1024;
|
|
363
473
|
const timeout = options?.timeout || this.DEFAULT_NETWORK_TIMEOUT;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
for await (const chunk of response.body) {
|
|
376
|
-
totalSize += chunk.length;
|
|
377
|
-
if (totalSize > maxSize) {
|
|
378
|
-
throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
|
|
474
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
475
|
+
const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
476
|
+
return withRetry(async () => {
|
|
477
|
+
const response = await request(url, {
|
|
478
|
+
dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
|
|
479
|
+
method: "GET",
|
|
480
|
+
headersTimeout: timeout,
|
|
481
|
+
bodyTimeout: timeout,
|
|
482
|
+
});
|
|
483
|
+
if (response.statusCode !== 200) {
|
|
484
|
+
throw new Error(`HTTP ${response.statusCode}`);
|
|
379
485
|
}
|
|
380
|
-
chunks
|
|
381
|
-
|
|
382
|
-
|
|
486
|
+
const chunks = [];
|
|
487
|
+
let totalSize = 0;
|
|
488
|
+
for await (const chunk of response.body) {
|
|
489
|
+
totalSize += chunk.length;
|
|
490
|
+
if (totalSize > maxSize) {
|
|
491
|
+
throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
|
|
492
|
+
}
|
|
493
|
+
chunks.push(chunk);
|
|
494
|
+
}
|
|
495
|
+
return Buffer.concat(chunks);
|
|
496
|
+
}, { maxRetries, retryDelay });
|
|
383
497
|
}
|
|
384
498
|
/**
|
|
385
499
|
* Load file from filesystem path
|
|
@@ -309,6 +309,10 @@ export class PDFProcessor {
|
|
|
309
309
|
base64Length: base64Image.length,
|
|
310
310
|
});
|
|
311
311
|
}
|
|
312
|
+
// Check for empty PDF (0 pages)
|
|
313
|
+
if (images.length === 0) {
|
|
314
|
+
throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
|
|
315
|
+
}
|
|
312
316
|
const conversionTimeMs = Date.now() - startTime;
|
|
313
317
|
logger.info("[PDF→Image] ✅ PDF conversion completed", {
|
|
314
318
|
pageCount: images.length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "8.35.
|
|
3
|
+
"version": "8.35.2",
|
|
4
4
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Juspay Technologies",
|