@juspay/neurolink 10.10.8 → 10.10.9
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 +7 -0
- package/dist/browser/neurolink.min.js +307 -307
- package/dist/lib/skills/skillsManager.js +24 -5
- package/dist/lib/types/file.d.ts +4 -0
- package/dist/lib/types/generate.d.ts +15 -0
- package/dist/lib/types/stream.d.ts +11 -0
- package/dist/lib/utils/messageBuilder.js +40 -3
- package/dist/lib/utils/multimodalOptionsBuilder.d.ts +2 -0
- package/dist/lib/utils/pdfProcessor.js +10 -0
- package/dist/skills/skillsManager.js +24 -5
- package/dist/types/file.d.ts +4 -0
- package/dist/types/generate.d.ts +15 -0
- package/dist/types/stream.d.ts +11 -0
- package/dist/utils/messageBuilder.js +40 -3
- package/dist/utils/multimodalOptionsBuilder.d.ts +2 -0
- package/dist/utils/pdfProcessor.js +10 -0
- package/package.json +1 -1
|
@@ -87,7 +87,14 @@ export class SkillsManager {
|
|
|
87
87
|
// name-find would resolve non-deterministically to the stale deprecated one
|
|
88
88
|
// across store backends. Fall back to any match only when no active skill
|
|
89
89
|
// carries the name.
|
|
90
|
-
|
|
90
|
+
//
|
|
91
|
+
// Matched case-insensitively to agree with assertNameAvailable, which
|
|
92
|
+
// enforces uniqueness that way: names differing only in case cannot
|
|
93
|
+
// coexist, so a case-sensitive lookup could only fail to find a skill that
|
|
94
|
+
// is definitively there (`get("DEPLOY")` returned null for "deploy").
|
|
95
|
+
const wanted = idOrName.toLowerCase();
|
|
96
|
+
const entry = index.find((item) => item.name.toLowerCase() === wanted &&
|
|
97
|
+
(item.status ?? "active") === "active") ?? index.find((item) => item.name.toLowerCase() === wanted);
|
|
91
98
|
return entry ? this.store.get(entry.id) : null;
|
|
92
99
|
}
|
|
93
100
|
/**
|
|
@@ -237,11 +244,23 @@ export class SkillsManager {
|
|
|
237
244
|
}
|
|
238
245
|
async assertNameAvailable(name, excludeId) {
|
|
239
246
|
const index = await this.getIndex(true);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
// Deliberately NOT filtered to active (#1139). Soft-delete only flips
|
|
248
|
+
// status to "deprecated" — the entry stays in the index, so allowing a new
|
|
249
|
+
// skill to take the name left two entries sharing it. Every name-based
|
|
250
|
+
// lookup (CLI `skills show/delete <name>`, the skill_update/skill_delete
|
|
251
|
+
// tools, the `:id`-or-name REST routes) then had to guess which one the
|
|
252
|
+
// caller meant, and iteration order differs across store backends.
|
|
253
|
+
//
|
|
254
|
+
// A deprecated skill's name therefore stays reserved. Reusing it requires
|
|
255
|
+
// hard-deleting the old skill first, which is the explicit choice the
|
|
256
|
+
// ambiguity demands.
|
|
257
|
+
const clash = index.find((item) => item.id !== excludeId && item.name.toLowerCase() === name.toLowerCase());
|
|
243
258
|
if (clash) {
|
|
244
|
-
|
|
259
|
+
const suffix = (clash.status ?? "active") === "active"
|
|
260
|
+
? ""
|
|
261
|
+
: ` (that name belongs to a deprecated skill, id ${clash.id}; ` +
|
|
262
|
+
`remove it before reusing the name)`;
|
|
263
|
+
throw new Error(`A skill named "${name}" already exists${suffix}`);
|
|
245
264
|
}
|
|
246
265
|
}
|
|
247
266
|
}
|
package/dist/lib/types/file.d.ts
CHANGED
|
@@ -474,6 +474,10 @@ export type MultimodalPdfEntry = {
|
|
|
474
474
|
password?: string;
|
|
475
475
|
/** Per-page pixel ceiling for the image fallback (#260). */
|
|
476
476
|
maxCanvasPixels?: number;
|
|
477
|
+
/** Render scale for the image fallback (#297). */
|
|
478
|
+
scale?: number;
|
|
479
|
+
/** Max pages converted by the image fallback (#297). */
|
|
480
|
+
maxPages?: number;
|
|
477
481
|
};
|
|
478
482
|
/** Result of PDF to image conversion. */
|
|
479
483
|
export type PDFImageConversionResult = {
|
|
@@ -133,6 +133,17 @@ export type GenerateOptions = {
|
|
|
133
133
|
password?: string;
|
|
134
134
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
135
135
|
maxCanvasPixels?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Render scale for the image fallback used by providers without native PDF
|
|
138
|
+
* support (#297). Higher is sharper but costs roughly the square in memory
|
|
139
|
+
* and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
|
|
140
|
+
*/
|
|
141
|
+
scale?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Max pages converted by the image fallback (#297). Pages beyond this are
|
|
144
|
+
* not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
|
|
145
|
+
*/
|
|
146
|
+
maxPages?: number;
|
|
136
147
|
};
|
|
137
148
|
videoOptions?: {
|
|
138
149
|
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
@@ -1265,6 +1276,10 @@ export type TextGenerationOptions = {
|
|
|
1265
1276
|
password?: string;
|
|
1266
1277
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
1267
1278
|
maxCanvasPixels?: number;
|
|
1279
|
+
/** Render scale for the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_SCALE. */
|
|
1280
|
+
scale?: number;
|
|
1281
|
+
/** Max pages converted by the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_MAX_PAGES. */
|
|
1282
|
+
maxPages?: number;
|
|
1268
1283
|
};
|
|
1269
1284
|
enableSummarization?: boolean;
|
|
1270
1285
|
/**
|
|
@@ -225,6 +225,17 @@ export type StreamOptions = {
|
|
|
225
225
|
password?: string;
|
|
226
226
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
227
227
|
maxCanvasPixels?: number;
|
|
228
|
+
/**
|
|
229
|
+
* Render scale for the image fallback used by providers without native PDF
|
|
230
|
+
* support (#297). Higher is sharper but costs roughly the square in memory
|
|
231
|
+
* and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
|
|
232
|
+
*/
|
|
233
|
+
scale?: number;
|
|
234
|
+
/**
|
|
235
|
+
* Max pages converted by the image fallback (#297). Pages beyond this are
|
|
236
|
+
* not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
|
|
237
|
+
*/
|
|
238
|
+
maxPages?: number;
|
|
228
239
|
};
|
|
229
240
|
videoOptions?: {
|
|
230
241
|
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
@@ -4,6 +4,7 @@ import { getGlobalDispatcher, interceptors, request } from "undici";
|
|
|
4
4
|
import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerImageAdapter.js";
|
|
5
5
|
import { CONVERSATION_INSTRUCTIONS, STRUCTURED_OUTPUT_INSTRUCTIONS, } from "../config/conversationMemory.js";
|
|
6
6
|
import { getAvailableInputTokens } from "../constants/contextWindows.js";
|
|
7
|
+
import { PDF_LIMITS } from "../core/constants.js";
|
|
7
8
|
import { enforceAggregateFileBudget, FILE_READ_BUDGET_PERCENT, } from "../context/fileTokenBudget.js";
|
|
8
9
|
import { isCSVContent, SIZE_TIER_THRESHOLDS } from "../types/index.js";
|
|
9
10
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
@@ -1150,6 +1151,10 @@ async function processExplicitPdfFiles(options, maxSize, provider) {
|
|
|
1150
1151
|
// #260: carry the per-page canvas-pixel ceiling so the caller can
|
|
1151
1152
|
// raise (or lower) the memory guard for the image-fallback render.
|
|
1152
1153
|
maxCanvasPixels: options.pdfOptions?.maxCanvasPixels,
|
|
1154
|
+
// #297: render scale / page ceiling, so the lowered default is
|
|
1155
|
+
// actually reachable and callers can trade sharpness for memory.
|
|
1156
|
+
scale: options.pdfOptions?.scale,
|
|
1157
|
+
maxPages: options.pdfOptions?.maxPages,
|
|
1153
1158
|
});
|
|
1154
1159
|
logger.info(`[PDF] ✅ Queued for multimodal: ${filename} (${result.metadata?.estimatedPages ?? "unknown"} pages)`);
|
|
1155
1160
|
}
|
|
@@ -1458,6 +1463,8 @@ async function convertContentToProviderFormat(content, provider, _model, pdfOpti
|
|
|
1458
1463
|
// guard as the `input.pdfFiles` path (see `processExplicitPdfFiles`).
|
|
1459
1464
|
password: pdfOptions?.password,
|
|
1460
1465
|
maxCanvasPixels: pdfOptions?.maxCanvasPixels,
|
|
1466
|
+
scale: pdfOptions?.scale,
|
|
1467
|
+
maxPages: pdfOptions?.maxPages,
|
|
1461
1468
|
}));
|
|
1462
1469
|
// #309: same aggregate ceiling as `input.pdfFiles`. Without this, moving an
|
|
1463
1470
|
// over-limit payload from `input.pdfFiles` to `input.content` skipped the
|
|
@@ -1809,7 +1816,10 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
|
|
|
1809
1816
|
/**
|
|
1810
1817
|
* Convert multimodal content (images + PDFs) to provider format
|
|
1811
1818
|
*/
|
|
1812
|
-
async function convertMultimodalToProviderFormat(text, images,
|
|
1819
|
+
async function convertMultimodalToProviderFormat(text, images,
|
|
1820
|
+
// The canonical entry shape (#309) rather than a fourth copy of it inline —
|
|
1821
|
+
// which is what let the render knobs stop short of this function.
|
|
1822
|
+
pdfFiles, provider, model) {
|
|
1813
1823
|
const content = [
|
|
1814
1824
|
{ type: "text", text },
|
|
1815
1825
|
];
|
|
@@ -1843,14 +1853,41 @@ async function convertMultimodalToProviderFormat(text, images, pdfFiles, provide
|
|
|
1843
1853
|
logger.info(`[PDF→Image] Provider ${provider} doesn't support native PDF. Converting ${pdfFiles.length} PDF(s) to images...`);
|
|
1844
1854
|
for (const pdf of pdfFiles) {
|
|
1845
1855
|
try {
|
|
1856
|
+
const effectiveMaxPages = pdf.maxPages ?? PDF_LIMITS.DEFAULT_MAX_PAGES;
|
|
1846
1857
|
const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
|
|
1847
|
-
|
|
1848
|
-
|
|
1858
|
+
// #297: this is the only PDF→image call the product actually makes,
|
|
1859
|
+
// and it used to hardcode scale 2.0 — silently overriding the
|
|
1860
|
+
// lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
|
|
1861
|
+
// issue reports (a 100-page render at 2.0 is ~776MB; 1.5 is ~44%
|
|
1862
|
+
// fewer pixels per page). Callers can raise it back per request.
|
|
1863
|
+
scale: pdf.scale ?? PDF_LIMITS.DEFAULT_SCALE,
|
|
1864
|
+
// Page ceiling guards token overflow; also now caller-adjustable
|
|
1865
|
+
// rather than a constant nothing could reach.
|
|
1866
|
+
maxPages: effectiveMaxPages,
|
|
1849
1867
|
...(pdf.password ? { password: pdf.password } : {}), // #258
|
|
1850
1868
|
...(pdf.maxCanvasPixels
|
|
1851
1869
|
? { maxCanvasPixels: pdf.maxCanvasPixels }
|
|
1852
1870
|
: {}), // #260
|
|
1853
1871
|
});
|
|
1872
|
+
// The renderer stops at maxPages, so a longer document is silently
|
|
1873
|
+
// truncated — say so rather than letting the model answer from a
|
|
1874
|
+
// partial document as though it had the whole thing.
|
|
1875
|
+
//
|
|
1876
|
+
// Keyed on the cap being reached, not on pdf.pageCount: that field is
|
|
1877
|
+
// null whenever `input.content` omits `metadata.pages`, which is the
|
|
1878
|
+
// common case, so a page-count comparison would simply never fire
|
|
1879
|
+
// there. Reaching the cap is also unambiguous — a short count caused by
|
|
1880
|
+
// per-page render failures (#294 isolates those into `errors`) would
|
|
1881
|
+
// otherwise be misreported as a maxPages truncation.
|
|
1882
|
+
if (conversionResult.pageCount >= effectiveMaxPages) {
|
|
1883
|
+
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)} hit the ${effectiveMaxPages}-page ` +
|
|
1884
|
+
`conversion limit. Any pages beyond that were not sent — the model may be ` +
|
|
1885
|
+
`answering from a partial document. Raise pdfOptions.maxPages or split the file.`);
|
|
1886
|
+
}
|
|
1887
|
+
if (conversionResult.errors && conversionResult.errors.length > 0) {
|
|
1888
|
+
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${conversionResult.errors.length} page(s) ` +
|
|
1889
|
+
`failed to render and were omitted (page ${conversionResult.errors.map((e) => e.page).join(", ")}).`);
|
|
1890
|
+
}
|
|
1854
1891
|
logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
|
|
1855
1892
|
// Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
|
|
1856
1893
|
conversionResult.images.forEach((base64Image, pageIndex) => {
|
|
@@ -58,6 +58,8 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
|
|
|
58
58
|
pdfOptions: {
|
|
59
59
|
password?: string;
|
|
60
60
|
maxCanvasPixels?: number;
|
|
61
|
+
scale?: number;
|
|
62
|
+
maxPages?: number;
|
|
61
63
|
} | undefined;
|
|
62
64
|
systemPrompt: string | undefined;
|
|
63
65
|
conversationHistory: import("../types/conversation.js").ChatMessage[] | undefined;
|
|
@@ -377,6 +377,7 @@ export class PDFProcessor {
|
|
|
377
377
|
format,
|
|
378
378
|
scale,
|
|
379
379
|
maxCanvasPixels,
|
|
380
|
+
maxPages,
|
|
380
381
|
});
|
|
381
382
|
logger.debug("[PDF→Image] ✅ PDF validation passed", {
|
|
382
383
|
bufferSize: pdfBuffer.length,
|
|
@@ -535,6 +536,14 @@ export class PDFProcessor {
|
|
|
535
536
|
if (!Number.isFinite(opts.maxCanvasPixels) || opts.maxCanvasPixels <= 0) {
|
|
536
537
|
throw new Error(`Invalid maxCanvasPixels: ${opts.maxCanvasPixels}. Must be a finite number greater than 0.`);
|
|
537
538
|
}
|
|
539
|
+
// #297: maxPages became caller-controlled, and an unvalidated 0/-1/NaN
|
|
540
|
+
// silently converts nothing, surfacing later as a misleading
|
|
541
|
+
// "PDF has 0 pages" from deep inside the renderer. Reject it here where
|
|
542
|
+
// the message can still name the offending option.
|
|
543
|
+
if (opts.maxPages !== undefined &&
|
|
544
|
+
(!Number.isInteger(opts.maxPages) || opts.maxPages < 1)) {
|
|
545
|
+
throw new Error(`Invalid maxPages: ${opts.maxPages}. Must be a whole number of at least 1.`);
|
|
546
|
+
}
|
|
538
547
|
if (!pdfBuffer || pdfBuffer.length < 5) {
|
|
539
548
|
throw new Error("Invalid PDF: Buffer is too small or empty. " +
|
|
540
549
|
"A valid PDF must be at least 5 bytes (PDF header).");
|
|
@@ -570,6 +579,7 @@ export class PDFProcessor {
|
|
|
570
579
|
format,
|
|
571
580
|
scale,
|
|
572
581
|
maxCanvasPixels,
|
|
582
|
+
maxPages,
|
|
573
583
|
});
|
|
574
584
|
const pdfToImgModule = await import("pdf-to-img");
|
|
575
585
|
const pdf = pdfToImgModule.pdf;
|
|
@@ -87,7 +87,14 @@ export class SkillsManager {
|
|
|
87
87
|
// name-find would resolve non-deterministically to the stale deprecated one
|
|
88
88
|
// across store backends. Fall back to any match only when no active skill
|
|
89
89
|
// carries the name.
|
|
90
|
-
|
|
90
|
+
//
|
|
91
|
+
// Matched case-insensitively to agree with assertNameAvailable, which
|
|
92
|
+
// enforces uniqueness that way: names differing only in case cannot
|
|
93
|
+
// coexist, so a case-sensitive lookup could only fail to find a skill that
|
|
94
|
+
// is definitively there (`get("DEPLOY")` returned null for "deploy").
|
|
95
|
+
const wanted = idOrName.toLowerCase();
|
|
96
|
+
const entry = index.find((item) => item.name.toLowerCase() === wanted &&
|
|
97
|
+
(item.status ?? "active") === "active") ?? index.find((item) => item.name.toLowerCase() === wanted);
|
|
91
98
|
return entry ? this.store.get(entry.id) : null;
|
|
92
99
|
}
|
|
93
100
|
/**
|
|
@@ -237,11 +244,23 @@ export class SkillsManager {
|
|
|
237
244
|
}
|
|
238
245
|
async assertNameAvailable(name, excludeId) {
|
|
239
246
|
const index = await this.getIndex(true);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
// Deliberately NOT filtered to active (#1139). Soft-delete only flips
|
|
248
|
+
// status to "deprecated" — the entry stays in the index, so allowing a new
|
|
249
|
+
// skill to take the name left two entries sharing it. Every name-based
|
|
250
|
+
// lookup (CLI `skills show/delete <name>`, the skill_update/skill_delete
|
|
251
|
+
// tools, the `:id`-or-name REST routes) then had to guess which one the
|
|
252
|
+
// caller meant, and iteration order differs across store backends.
|
|
253
|
+
//
|
|
254
|
+
// A deprecated skill's name therefore stays reserved. Reusing it requires
|
|
255
|
+
// hard-deleting the old skill first, which is the explicit choice the
|
|
256
|
+
// ambiguity demands.
|
|
257
|
+
const clash = index.find((item) => item.id !== excludeId && item.name.toLowerCase() === name.toLowerCase());
|
|
243
258
|
if (clash) {
|
|
244
|
-
|
|
259
|
+
const suffix = (clash.status ?? "active") === "active"
|
|
260
|
+
? ""
|
|
261
|
+
: ` (that name belongs to a deprecated skill, id ${clash.id}; ` +
|
|
262
|
+
`remove it before reusing the name)`;
|
|
263
|
+
throw new Error(`A skill named "${name}" already exists${suffix}`);
|
|
245
264
|
}
|
|
246
265
|
}
|
|
247
266
|
}
|
package/dist/types/file.d.ts
CHANGED
|
@@ -474,6 +474,10 @@ export type MultimodalPdfEntry = {
|
|
|
474
474
|
password?: string;
|
|
475
475
|
/** Per-page pixel ceiling for the image fallback (#260). */
|
|
476
476
|
maxCanvasPixels?: number;
|
|
477
|
+
/** Render scale for the image fallback (#297). */
|
|
478
|
+
scale?: number;
|
|
479
|
+
/** Max pages converted by the image fallback (#297). */
|
|
480
|
+
maxPages?: number;
|
|
477
481
|
};
|
|
478
482
|
/** Result of PDF to image conversion. */
|
|
479
483
|
export type PDFImageConversionResult = {
|
package/dist/types/generate.d.ts
CHANGED
|
@@ -133,6 +133,17 @@ export type GenerateOptions = {
|
|
|
133
133
|
password?: string;
|
|
134
134
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
135
135
|
maxCanvasPixels?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Render scale for the image fallback used by providers without native PDF
|
|
138
|
+
* support (#297). Higher is sharper but costs roughly the square in memory
|
|
139
|
+
* and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
|
|
140
|
+
*/
|
|
141
|
+
scale?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Max pages converted by the image fallback (#297). Pages beyond this are
|
|
144
|
+
* not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
|
|
145
|
+
*/
|
|
146
|
+
maxPages?: number;
|
|
136
147
|
};
|
|
137
148
|
videoOptions?: {
|
|
138
149
|
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
@@ -1265,6 +1276,10 @@ export type TextGenerationOptions = {
|
|
|
1265
1276
|
password?: string;
|
|
1266
1277
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
1267
1278
|
maxCanvasPixels?: number;
|
|
1279
|
+
/** Render scale for the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_SCALE. */
|
|
1280
|
+
scale?: number;
|
|
1281
|
+
/** Max pages converted by the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_MAX_PAGES. */
|
|
1282
|
+
maxPages?: number;
|
|
1268
1283
|
};
|
|
1269
1284
|
enableSummarization?: boolean;
|
|
1270
1285
|
/**
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -225,6 +225,17 @@ export type StreamOptions = {
|
|
|
225
225
|
password?: string;
|
|
226
226
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
227
227
|
maxCanvasPixels?: number;
|
|
228
|
+
/**
|
|
229
|
+
* Render scale for the image fallback used by providers without native PDF
|
|
230
|
+
* support (#297). Higher is sharper but costs roughly the square in memory
|
|
231
|
+
* and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
|
|
232
|
+
*/
|
|
233
|
+
scale?: number;
|
|
234
|
+
/**
|
|
235
|
+
* Max pages converted by the image fallback (#297). Pages beyond this are
|
|
236
|
+
* not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
|
|
237
|
+
*/
|
|
238
|
+
maxPages?: number;
|
|
228
239
|
};
|
|
229
240
|
videoOptions?: {
|
|
230
241
|
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
@@ -4,6 +4,7 @@ import { getGlobalDispatcher, interceptors, request } from "undici";
|
|
|
4
4
|
import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerImageAdapter.js";
|
|
5
5
|
import { CONVERSATION_INSTRUCTIONS, STRUCTURED_OUTPUT_INSTRUCTIONS, } from "../config/conversationMemory.js";
|
|
6
6
|
import { getAvailableInputTokens } from "../constants/contextWindows.js";
|
|
7
|
+
import { PDF_LIMITS } from "../core/constants.js";
|
|
7
8
|
import { enforceAggregateFileBudget, FILE_READ_BUDGET_PERCENT, } from "../context/fileTokenBudget.js";
|
|
8
9
|
import { isCSVContent, SIZE_TIER_THRESHOLDS } from "../types/index.js";
|
|
9
10
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
@@ -1150,6 +1151,10 @@ async function processExplicitPdfFiles(options, maxSize, provider) {
|
|
|
1150
1151
|
// #260: carry the per-page canvas-pixel ceiling so the caller can
|
|
1151
1152
|
// raise (or lower) the memory guard for the image-fallback render.
|
|
1152
1153
|
maxCanvasPixels: options.pdfOptions?.maxCanvasPixels,
|
|
1154
|
+
// #297: render scale / page ceiling, so the lowered default is
|
|
1155
|
+
// actually reachable and callers can trade sharpness for memory.
|
|
1156
|
+
scale: options.pdfOptions?.scale,
|
|
1157
|
+
maxPages: options.pdfOptions?.maxPages,
|
|
1153
1158
|
});
|
|
1154
1159
|
logger.info(`[PDF] ✅ Queued for multimodal: ${filename} (${result.metadata?.estimatedPages ?? "unknown"} pages)`);
|
|
1155
1160
|
}
|
|
@@ -1458,6 +1463,8 @@ async function convertContentToProviderFormat(content, provider, _model, pdfOpti
|
|
|
1458
1463
|
// guard as the `input.pdfFiles` path (see `processExplicitPdfFiles`).
|
|
1459
1464
|
password: pdfOptions?.password,
|
|
1460
1465
|
maxCanvasPixels: pdfOptions?.maxCanvasPixels,
|
|
1466
|
+
scale: pdfOptions?.scale,
|
|
1467
|
+
maxPages: pdfOptions?.maxPages,
|
|
1461
1468
|
}));
|
|
1462
1469
|
// #309: same aggregate ceiling as `input.pdfFiles`. Without this, moving an
|
|
1463
1470
|
// over-limit payload from `input.pdfFiles` to `input.content` skipped the
|
|
@@ -1809,7 +1816,10 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
|
|
|
1809
1816
|
/**
|
|
1810
1817
|
* Convert multimodal content (images + PDFs) to provider format
|
|
1811
1818
|
*/
|
|
1812
|
-
async function convertMultimodalToProviderFormat(text, images,
|
|
1819
|
+
async function convertMultimodalToProviderFormat(text, images,
|
|
1820
|
+
// The canonical entry shape (#309) rather than a fourth copy of it inline —
|
|
1821
|
+
// which is what let the render knobs stop short of this function.
|
|
1822
|
+
pdfFiles, provider, model) {
|
|
1813
1823
|
const content = [
|
|
1814
1824
|
{ type: "text", text },
|
|
1815
1825
|
];
|
|
@@ -1843,14 +1853,41 @@ async function convertMultimodalToProviderFormat(text, images, pdfFiles, provide
|
|
|
1843
1853
|
logger.info(`[PDF→Image] Provider ${provider} doesn't support native PDF. Converting ${pdfFiles.length} PDF(s) to images...`);
|
|
1844
1854
|
for (const pdf of pdfFiles) {
|
|
1845
1855
|
try {
|
|
1856
|
+
const effectiveMaxPages = pdf.maxPages ?? PDF_LIMITS.DEFAULT_MAX_PAGES;
|
|
1846
1857
|
const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
|
|
1847
|
-
|
|
1848
|
-
|
|
1858
|
+
// #297: this is the only PDF→image call the product actually makes,
|
|
1859
|
+
// and it used to hardcode scale 2.0 — silently overriding the
|
|
1860
|
+
// lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
|
|
1861
|
+
// issue reports (a 100-page render at 2.0 is ~776MB; 1.5 is ~44%
|
|
1862
|
+
// fewer pixels per page). Callers can raise it back per request.
|
|
1863
|
+
scale: pdf.scale ?? PDF_LIMITS.DEFAULT_SCALE,
|
|
1864
|
+
// Page ceiling guards token overflow; also now caller-adjustable
|
|
1865
|
+
// rather than a constant nothing could reach.
|
|
1866
|
+
maxPages: effectiveMaxPages,
|
|
1849
1867
|
...(pdf.password ? { password: pdf.password } : {}), // #258
|
|
1850
1868
|
...(pdf.maxCanvasPixels
|
|
1851
1869
|
? { maxCanvasPixels: pdf.maxCanvasPixels }
|
|
1852
1870
|
: {}), // #260
|
|
1853
1871
|
});
|
|
1872
|
+
// The renderer stops at maxPages, so a longer document is silently
|
|
1873
|
+
// truncated — say so rather than letting the model answer from a
|
|
1874
|
+
// partial document as though it had the whole thing.
|
|
1875
|
+
//
|
|
1876
|
+
// Keyed on the cap being reached, not on pdf.pageCount: that field is
|
|
1877
|
+
// null whenever `input.content` omits `metadata.pages`, which is the
|
|
1878
|
+
// common case, so a page-count comparison would simply never fire
|
|
1879
|
+
// there. Reaching the cap is also unambiguous — a short count caused by
|
|
1880
|
+
// per-page render failures (#294 isolates those into `errors`) would
|
|
1881
|
+
// otherwise be misreported as a maxPages truncation.
|
|
1882
|
+
if (conversionResult.pageCount >= effectiveMaxPages) {
|
|
1883
|
+
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)} hit the ${effectiveMaxPages}-page ` +
|
|
1884
|
+
`conversion limit. Any pages beyond that were not sent — the model may be ` +
|
|
1885
|
+
`answering from a partial document. Raise pdfOptions.maxPages or split the file.`);
|
|
1886
|
+
}
|
|
1887
|
+
if (conversionResult.errors && conversionResult.errors.length > 0) {
|
|
1888
|
+
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${conversionResult.errors.length} page(s) ` +
|
|
1889
|
+
`failed to render and were omitted (page ${conversionResult.errors.map((e) => e.page).join(", ")}).`);
|
|
1890
|
+
}
|
|
1854
1891
|
logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
|
|
1855
1892
|
// Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
|
|
1856
1893
|
conversionResult.images.forEach((base64Image, pageIndex) => {
|
|
@@ -58,6 +58,8 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
|
|
|
58
58
|
pdfOptions: {
|
|
59
59
|
password?: string;
|
|
60
60
|
maxCanvasPixels?: number;
|
|
61
|
+
scale?: number;
|
|
62
|
+
maxPages?: number;
|
|
61
63
|
} | undefined;
|
|
62
64
|
systemPrompt: string | undefined;
|
|
63
65
|
conversationHistory: import("../index.js").ChatMessage[] | undefined;
|
|
@@ -377,6 +377,7 @@ export class PDFProcessor {
|
|
|
377
377
|
format,
|
|
378
378
|
scale,
|
|
379
379
|
maxCanvasPixels,
|
|
380
|
+
maxPages,
|
|
380
381
|
});
|
|
381
382
|
logger.debug("[PDF→Image] ✅ PDF validation passed", {
|
|
382
383
|
bufferSize: pdfBuffer.length,
|
|
@@ -535,6 +536,14 @@ export class PDFProcessor {
|
|
|
535
536
|
if (!Number.isFinite(opts.maxCanvasPixels) || opts.maxCanvasPixels <= 0) {
|
|
536
537
|
throw new Error(`Invalid maxCanvasPixels: ${opts.maxCanvasPixels}. Must be a finite number greater than 0.`);
|
|
537
538
|
}
|
|
539
|
+
// #297: maxPages became caller-controlled, and an unvalidated 0/-1/NaN
|
|
540
|
+
// silently converts nothing, surfacing later as a misleading
|
|
541
|
+
// "PDF has 0 pages" from deep inside the renderer. Reject it here where
|
|
542
|
+
// the message can still name the offending option.
|
|
543
|
+
if (opts.maxPages !== undefined &&
|
|
544
|
+
(!Number.isInteger(opts.maxPages) || opts.maxPages < 1)) {
|
|
545
|
+
throw new Error(`Invalid maxPages: ${opts.maxPages}. Must be a whole number of at least 1.`);
|
|
546
|
+
}
|
|
538
547
|
if (!pdfBuffer || pdfBuffer.length < 5) {
|
|
539
548
|
throw new Error("Invalid PDF: Buffer is too small or empty. " +
|
|
540
549
|
"A valid PDF must be at least 5 bytes (PDF header).");
|
|
@@ -570,6 +579,7 @@ export class PDFProcessor {
|
|
|
570
579
|
format,
|
|
571
580
|
scale,
|
|
572
581
|
maxCanvasPixels,
|
|
582
|
+
maxPages,
|
|
573
583
|
});
|
|
574
584
|
const pdfToImgModule = await import("pdf-to-img");
|
|
575
585
|
const pdf = pdfToImgModule.pdf;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "10.10.
|
|
3
|
+
"version": "10.10.9",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|