@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
|
@@ -22,6 +22,16 @@ export declare class CLICommandFactory {
|
|
|
22
22
|
private static processCliImages;
|
|
23
23
|
private static processCliCSVFiles;
|
|
24
24
|
private static processCliPDFFiles;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the PDF decryption password, preferring the NEUROLINK_PDF_PASSWORD
|
|
27
|
+
* env var over the `--pdf-password` flag. A plaintext CLI flag leaks into
|
|
28
|
+
* shell history, `ps`/process listings, and CI logs — the env var avoids
|
|
29
|
+
* that, matching the project's existing "credentials via env vars, not
|
|
30
|
+
* flags" stance for the loop session. The flag stays supported (dropping it
|
|
31
|
+
* would be a breaking CLI change), but using it prints a one-line stderr
|
|
32
|
+
* warning recommending the env var instead.
|
|
33
|
+
*/
|
|
34
|
+
private static resolvePdfPassword;
|
|
25
35
|
private static processCliFiles;
|
|
26
36
|
private static processCliVideoFiles;
|
|
27
37
|
private static processOptions;
|
|
@@ -156,6 +156,12 @@ export class CLICommandFactory {
|
|
|
156
156
|
type: "string",
|
|
157
157
|
description: "Add PDF file for analysis (can be used multiple times)",
|
|
158
158
|
},
|
|
159
|
+
"pdf-password": {
|
|
160
|
+
type: "string",
|
|
161
|
+
description: "Password for an encrypted PDF (used on the image-conversion path). " +
|
|
162
|
+
"Visible in shell history/process listings — prefer the " +
|
|
163
|
+
"NEUROLINK_PDF_PASSWORD env var instead.",
|
|
164
|
+
},
|
|
159
165
|
video: {
|
|
160
166
|
type: "string",
|
|
161
167
|
description: "Add video file for analysis (can be used multiple times) (MP4, WebM, MOV, AVI, MKV)",
|
|
@@ -793,6 +799,24 @@ export class CLICommandFactory {
|
|
|
793
799
|
// URLs are preserved as-is by resolveFilePaths
|
|
794
800
|
return resolveFilePaths(paths);
|
|
795
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Resolve the PDF decryption password, preferring the NEUROLINK_PDF_PASSWORD
|
|
804
|
+
* env var over the `--pdf-password` flag. A plaintext CLI flag leaks into
|
|
805
|
+
* shell history, `ps`/process listings, and CI logs — the env var avoids
|
|
806
|
+
* that, matching the project's existing "credentials via env vars, not
|
|
807
|
+
* flags" stance for the loop session. The flag stays supported (dropping it
|
|
808
|
+
* would be a breaking CLI change), but using it prints a one-line stderr
|
|
809
|
+
* warning recommending the env var instead.
|
|
810
|
+
*/
|
|
811
|
+
static resolvePdfPassword(argv) {
|
|
812
|
+
const flagValue = argv.pdfPassword;
|
|
813
|
+
const envValue = process.env.NEUROLINK_PDF_PASSWORD;
|
|
814
|
+
if (flagValue) {
|
|
815
|
+
process.stderr.write(chalk.yellow("⚠️ --pdf-password is visible in shell history and process listings. " +
|
|
816
|
+
"Prefer the NEUROLINK_PDF_PASSWORD environment variable instead.\n"));
|
|
817
|
+
}
|
|
818
|
+
return flagValue || envValue;
|
|
819
|
+
}
|
|
796
820
|
// Helper method to process CLI files with auto-detection
|
|
797
821
|
static processCliFiles(files) {
|
|
798
822
|
if (!files) {
|
|
@@ -2649,6 +2673,9 @@ export class CLICommandFactory {
|
|
|
2649
2673
|
const inputAudioFormat = inferAudioFormatFromPath(inputAudioPath);
|
|
2650
2674
|
const runGenerate = () => sdk.generate({
|
|
2651
2675
|
input: generateInput,
|
|
2676
|
+
pdfOptions: {
|
|
2677
|
+
password: CLICommandFactory.resolvePdfPassword(argv),
|
|
2678
|
+
},
|
|
2652
2679
|
csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
|
|
2653
2680
|
videoOptions: {
|
|
2654
2681
|
frames: argv.videoFrames,
|
|
@@ -2901,6 +2928,9 @@ export class CLICommandFactory {
|
|
|
2901
2928
|
...(videoFiles && { videoFiles }),
|
|
2902
2929
|
...(files && { files }),
|
|
2903
2930
|
},
|
|
2931
|
+
pdfOptions: {
|
|
2932
|
+
password: CLICommandFactory.resolvePdfPassword(argv),
|
|
2933
|
+
},
|
|
2904
2934
|
csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
|
|
2905
2935
|
videoOptions: {
|
|
2906
2936
|
frames: argv.videoFrames,
|
|
@@ -4,4 +4,4 @@ import type { OptionSchema, TextGenerationOptions } from "../../lib/types/index.
|
|
|
4
4
|
* This object provides metadata for validation and help text in the CLI loop.
|
|
5
5
|
* It is derived from the main TextGenerationOptions interface to ensure consistency.
|
|
6
6
|
*/
|
|
7
|
-
export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation">, OptionSchema>;
|
|
7
|
+
export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation">, OptionSchema>;
|
|
@@ -420,6 +420,7 @@ export class BaseProvider {
|
|
|
420
420
|
toolUsageContext: options.toolUsageContext,
|
|
421
421
|
context: options.context,
|
|
422
422
|
csvOptions: options.csvOptions,
|
|
423
|
+
pdfOptions: options.pdfOptions,
|
|
423
424
|
// Forward abort, tool filtering, and timeout options to prevent
|
|
424
425
|
// silent bypass when falling back from real streaming to fake streaming
|
|
425
426
|
abortSignal: options.abortSignal,
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -122,6 +122,11 @@ export declare const PDF_LIMITS: {
|
|
|
122
122
|
MAX_SIZE_MB: number;
|
|
123
123
|
DEFAULT_MAX_PAGES: number;
|
|
124
124
|
MAX_SCALE: number;
|
|
125
|
+
MIN_SCALE: number;
|
|
126
|
+
DEFAULT_SCALE: number;
|
|
127
|
+
PAGE_COUNT_TIMEOUT_MS: number;
|
|
128
|
+
DEFAULT_MAX_CANVAS_PIXELS: number;
|
|
129
|
+
MIN_EFFECTIVE_SCALE: number;
|
|
125
130
|
};
|
|
126
131
|
export declare const SYSTEM_LIMITS: {
|
|
127
132
|
MAX_PROMPT_LENGTH: number;
|
package/dist/core/constants.js
CHANGED
|
@@ -220,6 +220,25 @@ export const PDF_LIMITS = {
|
|
|
220
220
|
// Upper bound for the render scale factor. Above this, a single page can
|
|
221
221
|
// allocate hundreds of MB; scale <= 0 produces a degenerate viewport.
|
|
222
222
|
MAX_SCALE: 10,
|
|
223
|
+
// Lower bound for the render scale factor (#297). Below this the render is
|
|
224
|
+
// effectively unreadable; enforcing it makes the documented 0.1–10 range real
|
|
225
|
+
// (previously only scale <= 0 was rejected).
|
|
226
|
+
MIN_SCALE: 0.1,
|
|
227
|
+
// Default render scale (#297). Lowered from 2 → 1.5 to roughly halve the
|
|
228
|
+
// per-page canvas memory while staying legible for OCR/vision.
|
|
229
|
+
DEFAULT_SCALE: 1.5,
|
|
230
|
+
// Timeout (ms) for the accurate pdf-parse page-count probe (#287); on timeout
|
|
231
|
+
// the processor falls back to the regex estimate rather than blocking.
|
|
232
|
+
PAGE_COUNT_TIMEOUT_MS: 5000,
|
|
233
|
+
// Default per-page pixel ceiling for image conversion (#260). 16.7M px ×
|
|
234
|
+
// 4 bytes RGBA ≈ 64 MB per page — safely bounded. A larger page is
|
|
235
|
+
// uniformly downscaled to stay under this instead of allocating gigabytes.
|
|
236
|
+
DEFAULT_MAX_CANVAS_PIXELS: 16_777_216,
|
|
237
|
+
// Floor for the downscaled render scale (#260 follow-up). A crafted/malformed
|
|
238
|
+
// MediaBox can drive the estimated pixel count toward Infinity, which would
|
|
239
|
+
// otherwise collapse `effectiveScale` to 0 and hand `pdf-to-img` a degenerate
|
|
240
|
+
// viewport. Never let the downscale branch go below this.
|
|
241
|
+
MIN_EFFECTIVE_SCALE: 0.1,
|
|
223
242
|
};
|
|
224
243
|
// Performance and System Limits
|
|
225
244
|
export const SYSTEM_LIMITS = {
|
|
@@ -103,6 +103,7 @@ export class MessageBuilder {
|
|
|
103
103
|
files: input?.files,
|
|
104
104
|
},
|
|
105
105
|
csvOptions: options.csvOptions,
|
|
106
|
+
pdfOptions: options.pdfOptions,
|
|
106
107
|
provider: options.provider,
|
|
107
108
|
model: options.model,
|
|
108
109
|
temperature: options.temperature,
|
|
@@ -224,6 +225,7 @@ export class MessageBuilder {
|
|
|
224
225
|
files: input?.files,
|
|
225
226
|
},
|
|
226
227
|
csvOptions: options.csvOptions,
|
|
228
|
+
pdfOptions: options.pdfOptions,
|
|
227
229
|
provider: options.provider,
|
|
228
230
|
model: options.model,
|
|
229
231
|
temperature: options.temperature,
|
|
@@ -420,6 +420,7 @@ export class BaseProvider {
|
|
|
420
420
|
toolUsageContext: options.toolUsageContext,
|
|
421
421
|
context: options.context,
|
|
422
422
|
csvOptions: options.csvOptions,
|
|
423
|
+
pdfOptions: options.pdfOptions,
|
|
423
424
|
// Forward abort, tool filtering, and timeout options to prevent
|
|
424
425
|
// silent bypass when falling back from real streaming to fake streaming
|
|
425
426
|
abortSignal: options.abortSignal,
|
|
@@ -122,6 +122,11 @@ export declare const PDF_LIMITS: {
|
|
|
122
122
|
MAX_SIZE_MB: number;
|
|
123
123
|
DEFAULT_MAX_PAGES: number;
|
|
124
124
|
MAX_SCALE: number;
|
|
125
|
+
MIN_SCALE: number;
|
|
126
|
+
DEFAULT_SCALE: number;
|
|
127
|
+
PAGE_COUNT_TIMEOUT_MS: number;
|
|
128
|
+
DEFAULT_MAX_CANVAS_PIXELS: number;
|
|
129
|
+
MIN_EFFECTIVE_SCALE: number;
|
|
125
130
|
};
|
|
126
131
|
export declare const SYSTEM_LIMITS: {
|
|
127
132
|
MAX_PROMPT_LENGTH: number;
|
|
@@ -220,6 +220,25 @@ export const PDF_LIMITS = {
|
|
|
220
220
|
// Upper bound for the render scale factor. Above this, a single page can
|
|
221
221
|
// allocate hundreds of MB; scale <= 0 produces a degenerate viewport.
|
|
222
222
|
MAX_SCALE: 10,
|
|
223
|
+
// Lower bound for the render scale factor (#297). Below this the render is
|
|
224
|
+
// effectively unreadable; enforcing it makes the documented 0.1–10 range real
|
|
225
|
+
// (previously only scale <= 0 was rejected).
|
|
226
|
+
MIN_SCALE: 0.1,
|
|
227
|
+
// Default render scale (#297). Lowered from 2 → 1.5 to roughly halve the
|
|
228
|
+
// per-page canvas memory while staying legible for OCR/vision.
|
|
229
|
+
DEFAULT_SCALE: 1.5,
|
|
230
|
+
// Timeout (ms) for the accurate pdf-parse page-count probe (#287); on timeout
|
|
231
|
+
// the processor falls back to the regex estimate rather than blocking.
|
|
232
|
+
PAGE_COUNT_TIMEOUT_MS: 5000,
|
|
233
|
+
// Default per-page pixel ceiling for image conversion (#260). 16.7M px ×
|
|
234
|
+
// 4 bytes RGBA ≈ 64 MB per page — safely bounded. A larger page is
|
|
235
|
+
// uniformly downscaled to stay under this instead of allocating gigabytes.
|
|
236
|
+
DEFAULT_MAX_CANVAS_PIXELS: 16_777_216,
|
|
237
|
+
// Floor for the downscaled render scale (#260 follow-up). A crafted/malformed
|
|
238
|
+
// MediaBox can drive the estimated pixel count toward Infinity, which would
|
|
239
|
+
// otherwise collapse `effectiveScale` to 0 and hand `pdf-to-img` a degenerate
|
|
240
|
+
// viewport. Never let the downscale branch go below this.
|
|
241
|
+
MIN_EFFECTIVE_SCALE: 0.1,
|
|
223
242
|
};
|
|
224
243
|
// Performance and System Limits
|
|
225
244
|
export const SYSTEM_LIMITS = {
|
|
@@ -103,6 +103,7 @@ export class MessageBuilder {
|
|
|
103
103
|
files: input?.files,
|
|
104
104
|
},
|
|
105
105
|
csvOptions: options.csvOptions,
|
|
106
|
+
pdfOptions: options.pdfOptions,
|
|
106
107
|
provider: options.provider,
|
|
107
108
|
model: options.model,
|
|
108
109
|
temperature: options.temperature,
|
|
@@ -224,6 +225,7 @@ export class MessageBuilder {
|
|
|
224
225
|
files: input?.files,
|
|
225
226
|
},
|
|
226
227
|
csvOptions: options.csvOptions,
|
|
228
|
+
pdfOptions: options.pdfOptions,
|
|
227
229
|
provider: options.provider,
|
|
228
230
|
model: options.model,
|
|
229
231
|
temperature: options.temperature,
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -3899,11 +3899,15 @@ Current user's request: ${currentInput}`;
|
|
|
3899
3899
|
evaluationDomain: options.evaluationDomain,
|
|
3900
3900
|
toolUsageContext: options.toolUsageContext,
|
|
3901
3901
|
input: options.input,
|
|
3902
|
-
//
|
|
3903
|
-
// TextGenerationOptions — the message builder reads them
|
|
3904
|
-
// formatting
|
|
3905
|
-
//
|
|
3902
|
+
// Multimodal file-processing options must survive the reconstruction
|
|
3903
|
+
// into TextGenerationOptions — the message builder reads them downstream
|
|
3904
|
+
// (csv encoding/formatting/sanitization/parse timeout, PDF
|
|
3905
|
+
// password/decryption #258). Omitting them here silently dropped e.g.
|
|
3906
|
+
// every csvOptions field (incl. the CLI --csv-* flags) or
|
|
3907
|
+
// pdfOptions.password so the CLI --pdf-password / SDK pdfOptions never
|
|
3908
|
+
// reached convertToImages.
|
|
3906
3909
|
csvOptions: options.csvOptions,
|
|
3910
|
+
pdfOptions: options.pdfOptions,
|
|
3907
3911
|
region: options.region,
|
|
3908
3912
|
tts: options.tts,
|
|
3909
3913
|
stt: options.stt,
|
package/dist/lib/types/file.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ export type FileProcessingResult = {
|
|
|
95
95
|
estimatedPages?: number | null;
|
|
96
96
|
provider?: string;
|
|
97
97
|
apiType?: PDFAPIType;
|
|
98
|
+
/** Provider's citations requirement for visual PDF analysis (#349). */
|
|
99
|
+
requiresCitations?: boolean | "auto";
|
|
98
100
|
officeFormat?: OfficeDocumentType;
|
|
99
101
|
pageCount?: number;
|
|
100
102
|
slideCount?: number;
|
|
@@ -210,6 +212,14 @@ export type PDFProviderConfig = {
|
|
|
210
212
|
maxSizeMB: number;
|
|
211
213
|
maxPages: number;
|
|
212
214
|
supportsNative: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Whether this provider needs source citations enabled for visual PDF
|
|
217
|
+
* analysis (#349). `"auto"` = enable when the request requires visual
|
|
218
|
+
* grounding (currently Bedrock's Converse document blocks); `false` = the
|
|
219
|
+
* provider handles PDFs without an explicit citations flag. Surfaced on
|
|
220
|
+
* `FileProcessingResult.metadata.requiresCitations` so downstream provider
|
|
221
|
+
* adapters can act on it instead of the value being dead config.
|
|
222
|
+
*/
|
|
213
223
|
requiresCitations: boolean | "auto";
|
|
214
224
|
apiType: PDFAPIType;
|
|
215
225
|
};
|
|
@@ -226,6 +236,8 @@ export type PDFProcessorOptions = {
|
|
|
226
236
|
* Set to false to bypass limit enforcement (logs warning instead)
|
|
227
237
|
*/
|
|
228
238
|
enforceLimits?: boolean;
|
|
239
|
+
/** Password for an encrypted PDF (used on the image-conversion path) (#258). */
|
|
240
|
+
password?: string;
|
|
229
241
|
};
|
|
230
242
|
/**
|
|
231
243
|
* Audio provider configuration for transcription services
|
|
@@ -395,10 +407,40 @@ export type PDFImageConversionOptions = {
|
|
|
395
407
|
maxPages?: number;
|
|
396
408
|
/** Output format (default: png). Only PNG is currently implemented by PDFProcessor. */
|
|
397
409
|
format?: "png";
|
|
410
|
+
/**
|
|
411
|
+
* Per-page pixel ceiling (#260). Any page whose width×height×scale² would
|
|
412
|
+
* exceed this is uniformly downscaled to stay under it, preventing a huge
|
|
413
|
+
* page from allocating gigabytes of canvas. Default: PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS.
|
|
414
|
+
*/
|
|
415
|
+
maxCanvasPixels?: number;
|
|
416
|
+
/** Password for an encrypted PDF (passed to the underlying renderer) (#258). */
|
|
417
|
+
password?: string;
|
|
418
|
+
/** Per-page progress callback invoked as each page is rendered (#302). */
|
|
419
|
+
onProgress?: (progress: PDFImageConversionProgress) => void | Promise<void>;
|
|
420
|
+
};
|
|
421
|
+
/** Progress reported per page during streaming conversion (#302). */
|
|
422
|
+
export type PDFImageConversionProgress = {
|
|
423
|
+
/** Number of pages successfully converted so far. */
|
|
424
|
+
pagesConverted: number;
|
|
425
|
+
/** Total pages in the document (known up-front from the renderer). */
|
|
426
|
+
totalPages: number;
|
|
427
|
+
/** Elapsed time since conversion started, in milliseconds. */
|
|
428
|
+
elapsedMs: number;
|
|
429
|
+
};
|
|
430
|
+
/** A single streamed page result (#302). `error` is set when that page failed. */
|
|
431
|
+
export type PDFImagePage = {
|
|
432
|
+
/** 1-based page index. */
|
|
433
|
+
pageIndex: number;
|
|
434
|
+
/** Base64-encoded PNG for the page (empty string when `error` is set). */
|
|
435
|
+
image: string;
|
|
436
|
+
/** Byte size of the rendered PNG (0 when `error` is set). */
|
|
437
|
+
imageSizeBytes: number;
|
|
438
|
+
/** Populated when this page failed to render (#294). */
|
|
439
|
+
error?: string;
|
|
398
440
|
};
|
|
399
441
|
/** Result of PDF to image conversion. */
|
|
400
442
|
export type PDFImageConversionResult = {
|
|
401
|
-
/** Array of base64-encoded PNG images (one per page) */
|
|
443
|
+
/** Array of base64-encoded PNG images (one per successfully converted page) */
|
|
402
444
|
images: string[];
|
|
403
445
|
/** Number of pages converted */
|
|
404
446
|
pageCount: number;
|
|
@@ -406,6 +448,11 @@ export type PDFImageConversionResult = {
|
|
|
406
448
|
conversionTimeMs: number;
|
|
407
449
|
/** Any warnings during conversion */
|
|
408
450
|
warnings?: string[];
|
|
451
|
+
/** Per-page failures — present only when some pages failed to render (#294). */
|
|
452
|
+
errors?: Array<{
|
|
453
|
+
page: number;
|
|
454
|
+
error: string;
|
|
455
|
+
}>;
|
|
409
456
|
};
|
|
410
457
|
/** Options for filename sanitization. */
|
|
411
458
|
export type SanitizeFileNameOptions = {
|
|
@@ -125,6 +125,13 @@ export type GenerateOptions = {
|
|
|
125
125
|
music?: MusicOptions;
|
|
126
126
|
};
|
|
127
127
|
csvOptions?: CSVProcessorOptions;
|
|
128
|
+
/** PDF processing options (#258). */
|
|
129
|
+
pdfOptions?: {
|
|
130
|
+
/** Password for an encrypted PDF (image-conversion fallback path). */
|
|
131
|
+
password?: string;
|
|
132
|
+
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
133
|
+
maxCanvasPixels?: number;
|
|
134
|
+
};
|
|
128
135
|
videoOptions?: {
|
|
129
136
|
frames?: number;
|
|
130
137
|
quality?: number;
|
|
@@ -1131,6 +1138,13 @@ export type TextGenerationOptions = {
|
|
|
1131
1138
|
expectedOutcome?: string;
|
|
1132
1139
|
evaluationCriteria?: string[];
|
|
1133
1140
|
csvOptions?: CSVProcessorOptions;
|
|
1141
|
+
/** PDF processing options (#258). */
|
|
1142
|
+
pdfOptions?: {
|
|
1143
|
+
/** Password for an encrypted PDF (image-conversion fallback path). */
|
|
1144
|
+
password?: string;
|
|
1145
|
+
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
1146
|
+
maxCanvasPixels?: number;
|
|
1147
|
+
};
|
|
1134
1148
|
enableSummarization?: boolean;
|
|
1135
1149
|
/**
|
|
1136
1150
|
* Skip injecting tool schemas into the system prompt.
|
|
@@ -207,6 +207,13 @@ export type StreamOptions = {
|
|
|
207
207
|
};
|
|
208
208
|
};
|
|
209
209
|
csvOptions?: CSVProcessorOptions;
|
|
210
|
+
/** PDF processing options (#258). */
|
|
211
|
+
pdfOptions?: {
|
|
212
|
+
/** Password for an encrypted PDF (image-conversion fallback path). */
|
|
213
|
+
password?: string;
|
|
214
|
+
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
215
|
+
maxCanvasPixels?: number;
|
|
216
|
+
};
|
|
210
217
|
videoOptions?: {
|
|
211
218
|
frames?: number;
|
|
212
219
|
quality?: number;
|
|
@@ -39,6 +39,8 @@ export declare const ERROR_CODES: {
|
|
|
39
39
|
readonly FILE_PROCESSING_FAILED: "FILE_PROCESSING_FAILED";
|
|
40
40
|
readonly CSV_PROCESSING_FAILED: "CSV_PROCESSING_FAILED";
|
|
41
41
|
readonly PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED";
|
|
42
|
+
readonly PDF_PASSWORD_REQUIRED: "PDF_PASSWORD_REQUIRED";
|
|
43
|
+
readonly PDF_INCORRECT_PASSWORD: "PDF_INCORRECT_PASSWORD";
|
|
42
44
|
readonly RATE_LIMITER_QUEUE_FULL: "RATE_LIMITER_QUEUE_FULL";
|
|
43
45
|
readonly RATE_LIMITER_QUEUE_TIMEOUT: "RATE_LIMITER_QUEUE_TIMEOUT";
|
|
44
46
|
readonly RATE_LIMITER_RESET: "RATE_LIMITER_RESET";
|
|
@@ -199,6 +201,14 @@ export declare class ErrorFactory {
|
|
|
199
201
|
* Create a PDF page limit exceeded error
|
|
200
202
|
*/
|
|
201
203
|
static pdfPageLimitExceeded(estimatedPages: number, maxPages: number, provider: string): NeuroLinkError;
|
|
204
|
+
/**
|
|
205
|
+
* The PDF is encrypted and no password was supplied (#258).
|
|
206
|
+
*/
|
|
207
|
+
static pdfPasswordRequired(): NeuroLinkError;
|
|
208
|
+
/**
|
|
209
|
+
* A password was supplied for an encrypted PDF but it was incorrect (#258).
|
|
210
|
+
*/
|
|
211
|
+
static pdfIncorrectPassword(): NeuroLinkError;
|
|
202
212
|
/**
|
|
203
213
|
* Create an image too large error
|
|
204
214
|
*/
|
|
@@ -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("../types/file.js").CSVProcessorOptions | undefined;
|
|
54
|
+
pdfOptions: {
|
|
55
|
+
password?: string;
|
|
56
|
+
maxCanvasPixels?: number;
|
|
57
|
+
} | undefined;
|
|
54
58
|
systemPrompt: string | undefined;
|
|
55
59
|
conversationHistory: import("../types/conversation.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,
|