@juspay/neurolink 9.95.1 → 9.95.3
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/browser/neurolink.min.js +352 -352
- package/dist/cli/factories/commandFactory.d.ts +10 -0
- package/dist/cli/factories/commandFactory.js +30 -0
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/core/baseProvider.js +1 -0
- package/dist/core/constants.d.ts +5 -0
- package/dist/core/constants.js +19 -0
- package/dist/core/modules/MessageBuilder.js +2 -0
- package/dist/lib/core/baseProvider.js +1 -0
- package/dist/lib/core/constants.d.ts +5 -0
- package/dist/lib/core/constants.js +19 -0
- package/dist/lib/core/modules/MessageBuilder.js +2 -0
- package/dist/lib/neurolink.js +8 -4
- package/dist/lib/types/file.d.ts +48 -1
- package/dist/lib/types/generate.d.ts +14 -0
- package/dist/lib/types/stream.d.ts +7 -0
- package/dist/lib/utils/errorHandling.d.ts +10 -0
- package/dist/lib/utils/errorHandling.js +28 -0
- package/dist/lib/utils/fileDetector.js +35 -0
- package/dist/lib/utils/messageBuilder.js +53 -5
- package/dist/lib/utils/multimodalOptionsBuilder.d.ts +4 -0
- package/dist/lib/utils/multimodalOptionsBuilder.js +1 -0
- package/dist/lib/utils/pdfProcessor.d.ts +39 -1
- package/dist/lib/utils/pdfProcessor.js +267 -48
- package/dist/neurolink.js +8 -4
- package/dist/types/file.d.ts +48 -1
- package/dist/types/generate.d.ts +14 -0
- package/dist/types/stream.d.ts +7 -0
- package/dist/utils/errorHandling.d.ts +10 -0
- package/dist/utils/errorHandling.js +28 -0
- package/dist/utils/fileDetector.js +35 -0
- package/dist/utils/messageBuilder.js +53 -5
- package/dist/utils/multimodalOptionsBuilder.d.ts +4 -0
- package/dist/utils/multimodalOptionsBuilder.js +1 -0
- package/dist/utils/pdfProcessor.d.ts +39 -1
- package/dist/utils/pdfProcessor.js +267 -48
- package/package.json +1 -1
|
@@ -52,6 +52,8 @@ export const ERROR_CODES = {
|
|
|
52
52
|
CSV_PROCESSING_FAILED: "CSV_PROCESSING_FAILED",
|
|
53
53
|
// PDF validation errors
|
|
54
54
|
PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED",
|
|
55
|
+
PDF_PASSWORD_REQUIRED: "PDF_PASSWORD_REQUIRED",
|
|
56
|
+
PDF_INCORRECT_PASSWORD: "PDF_INCORRECT_PASSWORD",
|
|
55
57
|
// Rate limiter errors
|
|
56
58
|
RATE_LIMITER_QUEUE_FULL: "RATE_LIMITER_QUEUE_FULL",
|
|
57
59
|
RATE_LIMITER_QUEUE_TIMEOUT: "RATE_LIMITER_QUEUE_TIMEOUT",
|
|
@@ -553,6 +555,32 @@ export class ErrorFactory {
|
|
|
553
555
|
},
|
|
554
556
|
});
|
|
555
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* The PDF is encrypted and no password was supplied (#258).
|
|
560
|
+
*/
|
|
561
|
+
static pdfPasswordRequired() {
|
|
562
|
+
return new NeuroLinkError({
|
|
563
|
+
code: ERROR_CODES.PDF_PASSWORD_REQUIRED,
|
|
564
|
+
message: "This PDF is password-protected. Supply the password via " +
|
|
565
|
+
"`pdfOptions: { password: '…' }` (SDK) or `--pdf-password` (CLI).",
|
|
566
|
+
category: ErrorCategory.VALIDATION,
|
|
567
|
+
severity: ErrorSeverity.MEDIUM,
|
|
568
|
+
retriable: false,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* A password was supplied for an encrypted PDF but it was incorrect (#258).
|
|
573
|
+
*/
|
|
574
|
+
static pdfIncorrectPassword() {
|
|
575
|
+
return new NeuroLinkError({
|
|
576
|
+
code: ERROR_CODES.PDF_INCORRECT_PASSWORD,
|
|
577
|
+
message: "The password supplied for this PDF is incorrect. Check the " +
|
|
578
|
+
"`pdfOptions.password` / `--pdf-password` value and try again.",
|
|
579
|
+
category: ErrorCategory.VALIDATION,
|
|
580
|
+
severity: ErrorSeverity.MEDIUM,
|
|
581
|
+
retriable: false,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
556
584
|
/**
|
|
557
585
|
* Create an image too large error
|
|
558
586
|
*/
|
|
@@ -1413,6 +1413,41 @@ export class FileDetector {
|
|
|
1413
1413
|
const timeout = options?.timeout || FileDetector.DEFAULT_NETWORK_TIMEOUT;
|
|
1414
1414
|
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1415
1415
|
const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
1416
|
+
// #317: pre-flight HEAD to reject an oversized file BEFORE downloading any
|
|
1417
|
+
// body. content-length is advisory (chunked responses omit it), so a
|
|
1418
|
+
// missing/invalid header — or a server that refuses HEAD — falls through to
|
|
1419
|
+
// the streaming byte guard below; only a genuine oversize rejection stops
|
|
1420
|
+
// the GET from ever running.
|
|
1421
|
+
//
|
|
1422
|
+
// #323: skip the pre-flight entirely when this exact URL was recently seen
|
|
1423
|
+
// (its Content-Type is still cached, whether from a prior loadFromURL GET
|
|
1424
|
+
// or a MimeTypeStrategy HEAD) — issuing a fresh HEAD here would defeat the
|
|
1425
|
+
// whole point of that cache. The streaming byte guard in the GET below
|
|
1426
|
+
// still enforces maxSize even without a pre-flight, so this doesn't remove
|
|
1427
|
+
// the oversize protection — it only skips the redundant round-trip for a
|
|
1428
|
+
// URL we've already been talking to within the last 60s.
|
|
1429
|
+
if (getCachedUrlContentType(url, Date.now()) === undefined) {
|
|
1430
|
+
try {
|
|
1431
|
+
const head = await request(url, {
|
|
1432
|
+
dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
|
|
1433
|
+
method: "HEAD",
|
|
1434
|
+
headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
|
|
1435
|
+
bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
|
|
1436
|
+
});
|
|
1437
|
+
// Drain/close the (empty) HEAD body so the connection can be reused.
|
|
1438
|
+
await head.body.dump();
|
|
1439
|
+
const declaredLength = Number(head.headers["content-length"]);
|
|
1440
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxSize) {
|
|
1441
|
+
throw new Error(`File too large: ${formatFileSize(declaredLength)} (max: ${formatFileSize(maxSize)})`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
catch (error) {
|
|
1445
|
+
if (error instanceof Error && /File too large/.test(error.message)) {
|
|
1446
|
+
throw error;
|
|
1447
|
+
}
|
|
1448
|
+
logger.debug(`[FileDetector] HEAD pre-flight skipped for ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1416
1451
|
return withRetry(async () => {
|
|
1417
1452
|
try {
|
|
1418
1453
|
const response = await request(url, {
|
|
@@ -1011,6 +1011,12 @@ async function processExplicitPdfFiles(options, maxSize, provider) {
|
|
|
1011
1011
|
buffer: result.content,
|
|
1012
1012
|
filename,
|
|
1013
1013
|
pageCount: result.metadata?.estimatedPages ?? null,
|
|
1014
|
+
// #258: carry the password so the image-fallback conversion can
|
|
1015
|
+
// decrypt an encrypted PDF for providers without native PDF support.
|
|
1016
|
+
password: options.pdfOptions?.password,
|
|
1017
|
+
// #260: carry the per-page canvas-pixel ceiling so the caller can
|
|
1018
|
+
// raise (or lower) the memory guard for the image-fallback render.
|
|
1019
|
+
maxCanvasPixels: options.pdfOptions?.maxCanvasPixels,
|
|
1014
1020
|
});
|
|
1015
1021
|
logger.info(`[PDF] ✅ Queued for multimodal: ${filename} (${result.metadata?.estimatedPages ?? "unknown"} pages)`);
|
|
1016
1022
|
}
|
|
@@ -1020,6 +1026,23 @@ async function processExplicitPdfFiles(options, maxSize, provider) {
|
|
|
1020
1026
|
throw error;
|
|
1021
1027
|
}
|
|
1022
1028
|
}
|
|
1029
|
+
// #309: enforce the provider's page/size ceilings across ALL PDFs, not just
|
|
1030
|
+
// per-file. N files each just under the single-file limit can still blow past
|
|
1031
|
+
// it in aggregate (e.g. three 40-page PDFs → 120 pages for a 100-page API).
|
|
1032
|
+
const aggregateConfig = PDFProcessor.getProviderConfig(provider);
|
|
1033
|
+
if (aggregateConfig && pdfFiles.length > 1) {
|
|
1034
|
+
const totalPages = pdfFiles.reduce((sum, f) => sum + (f.pageCount ?? 0), 0);
|
|
1035
|
+
const totalMB = pdfFiles.reduce((sum, f) => sum + f.buffer.length, 0) / (1024 * 1024);
|
|
1036
|
+
if (totalPages > aggregateConfig.maxPages) {
|
|
1037
|
+
throw new Error(`[PDF] Combined page count across ${pdfFiles.length} PDFs (${totalPages}) exceeds the ` +
|
|
1038
|
+
`${aggregateConfig.maxPages}-page limit for ${provider}. ` +
|
|
1039
|
+
`Split the request or reduce the number of PDFs.`);
|
|
1040
|
+
}
|
|
1041
|
+
if (totalMB > aggregateConfig.maxSizeMB) {
|
|
1042
|
+
throw new Error(`[PDF] Combined size across ${pdfFiles.length} PDFs (${totalMB.toFixed(2)}MB) exceeds the ` +
|
|
1043
|
+
`${aggregateConfig.maxSizeMB}MB limit for ${provider}.`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1023
1046
|
return pdfFiles;
|
|
1024
1047
|
}
|
|
1025
1048
|
/**
|
|
@@ -1103,7 +1126,13 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1103
1126
|
// Check if this is a multimodal request
|
|
1104
1127
|
const hasImages = (inp.images && inp.images.length > 0) ||
|
|
1105
1128
|
(inp.content && inp.content.some((c) => c.type === "image"));
|
|
1106
|
-
|
|
1129
|
+
// A PDF supplied only via input.content (type: "pdf", no explicit
|
|
1130
|
+
// input.pdfFiles and no image alongside it) must still route through the
|
|
1131
|
+
// multimodal path below — otherwise it silently falls through to the
|
|
1132
|
+
// text-only branch and the PDF (and any pdfOptions) never reaches
|
|
1133
|
+
// convertContentToProviderFormat at all.
|
|
1134
|
+
const hasPDFs = pdfFiles.length > 0 ||
|
|
1135
|
+
!!(inp.content && inp.content.some((c) => c.type === "pdf"));
|
|
1107
1136
|
// If no images or PDFs, use standard message building and convert to MultimodalChatMessage[]
|
|
1108
1137
|
if (!hasImages && !hasPDFs) {
|
|
1109
1138
|
// #289: CSV content[] items don't need vision, so they never reach the
|
|
@@ -1139,8 +1168,11 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1139
1168
|
`Supported providers: ${ProviderImageAdapter.getVisionProviders().join(", ")}`);
|
|
1140
1169
|
}
|
|
1141
1170
|
const messages = [];
|
|
1142
|
-
// Build enhanced system prompt
|
|
1143
|
-
|
|
1171
|
+
// Build enhanced system prompt. Gate on the same `hasPDFs` predicate used
|
|
1172
|
+
// for routing above — a PDF supplied only via input.content (no explicit
|
|
1173
|
+
// input.pdfFiles) must still get the "treat inlined content as an
|
|
1174
|
+
// attachment" instruction, or the model can claim no files were attached.
|
|
1175
|
+
const systemPrompt = buildMultimodalSystemPrompt(options, hasPDFs);
|
|
1144
1176
|
if (systemPrompt.trim()) {
|
|
1145
1177
|
messages.push({
|
|
1146
1178
|
role: "system",
|
|
@@ -1206,7 +1238,7 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1206
1238
|
try {
|
|
1207
1239
|
let userContent;
|
|
1208
1240
|
if (inp.content && inp.content.length > 0) {
|
|
1209
|
-
userContent = await convertContentToProviderFormat(inp.content, provider, model);
|
|
1241
|
+
userContent = await convertContentToProviderFormat(inp.content, provider, model, options.pdfOptions);
|
|
1210
1242
|
}
|
|
1211
1243
|
else if ((inp.images && inp.images.length > 0) || pdfFiles.length > 0) {
|
|
1212
1244
|
userContent = await convertMultimodalToProviderFormat(inp.text ?? "", inp.images || [], pdfFiles, provider, model);
|
|
@@ -1285,7 +1317,7 @@ async function appendCsvContentToText(csvItems, baseText) {
|
|
|
1285
1317
|
/**
|
|
1286
1318
|
* Convert advanced content format to provider-specific format
|
|
1287
1319
|
*/
|
|
1288
|
-
async function convertContentToProviderFormat(content, provider, _model) {
|
|
1320
|
+
async function convertContentToProviderFormat(content, provider, _model, pdfOptions) {
|
|
1289
1321
|
const textContent = content.find((c) => c.type === "text");
|
|
1290
1322
|
const imageContent = content.filter((c) => c.type === "image");
|
|
1291
1323
|
const pdfContent = content.filter((c) => c.type === "pdf");
|
|
@@ -1312,6 +1344,11 @@ async function convertContentToProviderFormat(content, provider, _model) {
|
|
|
1312
1344
|
buffer: typeof pdf.data === "string" ? Buffer.from(pdf.data, "base64") : pdf.data,
|
|
1313
1345
|
filename: pdf.metadata?.filename || "document.pdf",
|
|
1314
1346
|
pageCount: pdf.metadata?.pages ?? null,
|
|
1347
|
+
// #258/#260: carry password + canvas-pixel ceiling so a PDF supplied via
|
|
1348
|
+
// the advanced `input.content` array gets the same decryption/memory
|
|
1349
|
+
// guard as the `input.pdfFiles` path (see `processExplicitPdfFiles`).
|
|
1350
|
+
password: pdfOptions?.password,
|
|
1351
|
+
maxCanvasPixels: pdfOptions?.maxCanvasPixels,
|
|
1315
1352
|
}));
|
|
1316
1353
|
return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model);
|
|
1317
1354
|
}
|
|
@@ -1694,6 +1731,10 @@ async function convertMultimodalToProviderFormat(text, images, pdfFiles, provide
|
|
|
1694
1731
|
const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
|
|
1695
1732
|
scale: 2.0, // High quality for OCR/analysis
|
|
1696
1733
|
maxPages: 20, // Limit pages to prevent token overflow
|
|
1734
|
+
...(pdf.password ? { password: pdf.password } : {}), // #258
|
|
1735
|
+
...(pdf.maxCanvasPixels
|
|
1736
|
+
? { maxCanvasPixels: pdf.maxCanvasPixels }
|
|
1737
|
+
: {}), // #260
|
|
1697
1738
|
});
|
|
1698
1739
|
logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
|
|
1699
1740
|
// Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
|
|
@@ -1715,6 +1756,13 @@ async function convertMultimodalToProviderFormat(text, images, pdfFiles, provide
|
|
|
1715
1756
|
catch (error) {
|
|
1716
1757
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1717
1758
|
logger.error(`[PDF→Image] ❌ Failed to convert ${pdf.filename}: ${errorMessage}`);
|
|
1759
|
+
// #258: password errors are already actionable typed errors — re-throw
|
|
1760
|
+
// them unwrapped so the "supply a password" guidance isn't buried.
|
|
1761
|
+
const code = error?.code;
|
|
1762
|
+
if (code === "PDF_PASSWORD_REQUIRED" ||
|
|
1763
|
+
code === "PDF_INCORRECT_PASSWORD") {
|
|
1764
|
+
throw error;
|
|
1765
|
+
}
|
|
1718
1766
|
// Re-throw so the user knows PDF processing failed
|
|
1719
1767
|
throw new Error(`PDF to image conversion failed for ${pdf.filename}: ${errorMessage}. ` +
|
|
1720
1768
|
`Provider ${provider} doesn't support native PDFs and image conversion failed.`, { cause: error });
|
|
@@ -51,6 +51,10 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
|
|
|
51
51
|
pdfFiles: (string | Buffer<ArrayBufferLike>)[] | undefined;
|
|
52
52
|
};
|
|
53
53
|
csvOptions: import("../index.js").CSVProcessorOptions | undefined;
|
|
54
|
+
pdfOptions: {
|
|
55
|
+
password?: string;
|
|
56
|
+
maxCanvasPixels?: number;
|
|
57
|
+
} | undefined;
|
|
54
58
|
systemPrompt: string | undefined;
|
|
55
59
|
conversationHistory: import("../index.js").ChatMessage[] | undefined;
|
|
56
60
|
provider: string;
|
|
@@ -51,6 +51,7 @@ export function buildMultimodalOptions(options, providerName, modelName) {
|
|
|
51
51
|
pdfFiles: options.input?.pdfFiles,
|
|
52
52
|
},
|
|
53
53
|
csvOptions: options.csvOptions,
|
|
54
|
+
pdfOptions: options.pdfOptions,
|
|
54
55
|
systemPrompt: options.systemPrompt,
|
|
55
56
|
conversationHistory: options.conversationMessages,
|
|
56
57
|
provider: providerName,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The conversion uses pdf-to-img package (MuPDF-based) for high-quality conversion.
|
|
9
9
|
*/
|
|
10
|
-
import type { FileProcessingResult, PDFProcessorOptions, PDFProviderConfig, PDFImageConversionOptions, PDFImageConversionResult } from "../types/index.js";
|
|
10
|
+
import type { FileProcessingResult, PDFProcessorOptions, PDFProviderConfig, PDFImageConversionOptions, PDFImageConversionResult, PDFImagePage } from "../types/index.js";
|
|
11
11
|
export declare class PDFProcessor {
|
|
12
12
|
private static readonly PDF_SIGNATURE;
|
|
13
13
|
static process(content: Buffer, options?: PDFProcessorOptions): Promise<FileProcessingResult>;
|
|
@@ -20,7 +20,31 @@ export declare class PDFProcessor {
|
|
|
20
20
|
static getProviderConfig(provider: string): PDFProviderConfig | null;
|
|
21
21
|
private static isValidPDF;
|
|
22
22
|
private static extractBasicMetadata;
|
|
23
|
+
/**
|
|
24
|
+
* Accurate page count via pdf-parse (pdfjs) (#287). The header regex in
|
|
25
|
+
* `extractBasicMetadata` only sees plaintext `/Type /Page` markers and misses
|
|
26
|
+
* compressed/object-stream PDFs (and miscounts when a page dict spans a chunk
|
|
27
|
+
* boundary). This parses the document properly, bounded by a timeout so a
|
|
28
|
+
* pathological PDF can't block; returns null on timeout/failure so the caller
|
|
29
|
+
* falls back to the regex estimate.
|
|
30
|
+
*/
|
|
31
|
+
private static getAccuratePageCount;
|
|
23
32
|
static estimateTokens(pageCount: number, mode?: "text-only" | "visual"): number;
|
|
33
|
+
/**
|
|
34
|
+
* Estimate the largest page's rendered pixel count from the PDF's MediaBox
|
|
35
|
+
* entries, WITHOUT rendering (#260). Parses `/MediaBox [llx lly urx ury]`
|
|
36
|
+
* (dimensions in points); rendered pixels ≈ (width·scale)·(height·scale).
|
|
37
|
+
* Returns 0 when no plaintext MediaBox is found (e.g. compressed object
|
|
38
|
+
* streams) — the caller then skips downscaling, matching prior behavior.
|
|
39
|
+
*
|
|
40
|
+
* A crafted/malformed MediaBox can have finite but astronomically large
|
|
41
|
+
* width/height; `width * height * scale * scale` can then overflow past
|
|
42
|
+
* `Number.MAX_VALUE` to `Infinity`. Clamp the product to
|
|
43
|
+
* `Number.MAX_SAFE_INTEGER` so it stays finite — a large-but-finite pixel
|
|
44
|
+
* estimate still triggers downscaling, whereas `Infinity` would collapse
|
|
45
|
+
* the computed downscale factor to 0 (see `convertToImages`).
|
|
46
|
+
*/
|
|
47
|
+
private static largestPagePixels;
|
|
24
48
|
/**
|
|
25
49
|
* Convert a PDF buffer to an array of base64 PNG images
|
|
26
50
|
*
|
|
@@ -46,6 +70,20 @@ export declare class PDFProcessor {
|
|
|
46
70
|
* ```
|
|
47
71
|
*/
|
|
48
72
|
static convertToImages(pdfBuffer: Buffer, options?: PDFImageConversionOptions): Promise<PDFImageConversionResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Shared input validation for the image-conversion paths. Returns the PDF
|
|
75
|
+
* size in MB (needed by the memory-estimate log). Throws on any invalid input
|
|
76
|
+
* with the same messages the batch path has always used.
|
|
77
|
+
*/
|
|
78
|
+
private static validateImageConversionInput;
|
|
79
|
+
/**
|
|
80
|
+
* Streaming variant of {@link convertToImages} (#302): yields each page's
|
|
81
|
+
* base64 PNG as soon as it renders instead of buffering the whole document,
|
|
82
|
+
* and reports progress via `options.onProgress`. A page that fails to render
|
|
83
|
+
* is yielded with `error` set (per-page isolation, #294) rather than aborting
|
|
84
|
+
* the stream. `convertToImages` is the batch wrapper over this contract.
|
|
85
|
+
*/
|
|
86
|
+
static convertToImagesStream(pdfBuffer: Buffer, options?: PDFImageConversionOptions): AsyncGenerator<PDFImagePage, void, void>;
|
|
49
87
|
/**
|
|
50
88
|
* Convert a PDF file path to an array of base64 PNG images
|
|
51
89
|
*
|