@juspay/neurolink 12.21.2 → 12.22.1
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 +2 -4
- package/dist/browser/neurolink.min.js +393 -393
- package/dist/core/baseProvider.d.ts +18 -0
- package/dist/core/baseProvider.js +22 -0
- package/dist/core/constants.js +1 -1
- package/dist/factories/providerFactory.d.ts +20 -0
- package/dist/factories/providerFactory.js +100 -19
- package/dist/models/modelRegistry.d.ts +25 -0
- package/dist/models/modelRegistry.js +39 -0
- package/dist/providers/googleAiStudio/client.js +382 -50
- package/dist/providers/googleNativeGemini3/utils.d.ts +22 -0
- package/dist/providers/googleNativeGemini3/utils.js +24 -0
- package/dist/providers/googleVertex/client.d.ts +3 -12
- package/dist/providers/googleVertex/client.js +15 -34
- package/dist/providers/openaiChatCompletionsClient.d.ts +4 -0
- package/dist/providers/openaiChatCompletionsClient.js +20 -5
- package/dist/types/file.d.ts +13 -0
- package/dist/utils/pdfProcessor.d.ts +21 -3
- package/dist/utils/pdfProcessor.js +124 -48
- package/dist/utils/stringDistance.d.ts +54 -0
- package/dist/utils/stringDistance.js +133 -0
- package/dist/utils/toolCallRepair.js +54 -55
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -3
|
@@ -250,6 +250,28 @@ export declare function buildTurnStalledMessage(stallTimeoutMs: number, toolCall
|
|
|
250
250
|
* short-circuit — stopReason "aborted").
|
|
251
251
|
*/
|
|
252
252
|
export declare function buildAbortedTurnMessage(toolCallCount: number): string;
|
|
253
|
+
/**
|
|
254
|
+
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
255
|
+
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
256
|
+
* only — time/stall/abort exits must never claim a step limit was reached
|
|
257
|
+
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
258
|
+
* abort wearing the cap message).
|
|
259
|
+
*
|
|
260
|
+
* Takes plain booleans rather than a turn clock so a loop whose wall-clock
|
|
261
|
+
* deadline is owned by something other than `createTurnClock` — AI Studio's
|
|
262
|
+
* pre-existing per-request timeout controller — can report through the same
|
|
263
|
+
* builders instead of growing a second set.
|
|
264
|
+
*/
|
|
265
|
+
export declare function buildLoopExitMessage(params: {
|
|
266
|
+
timedOut: boolean;
|
|
267
|
+
stalled: boolean;
|
|
268
|
+
/** Turn duration so far, for the time-limit message. */
|
|
269
|
+
elapsedMs: number;
|
|
270
|
+
wasAborted: boolean;
|
|
271
|
+
stallTimeoutMs?: number;
|
|
272
|
+
maxSteps: number;
|
|
273
|
+
toolCallCount: number;
|
|
274
|
+
}): string;
|
|
253
275
|
/**
|
|
254
276
|
* Wrap-up nudge injected when the remaining turn time drops inside the
|
|
255
277
|
* `wrapupTimeLeadMs` window — the time-budget twin of the soft step-budget
|
|
@@ -922,6 +922,30 @@ export function buildAbortedTurnMessage(toolCallCount) {
|
|
|
922
922
|
: "";
|
|
923
923
|
return `This turn was stopped before I could finish.${calls}`;
|
|
924
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
927
|
+
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
928
|
+
* only — time/stall/abort exits must never claim a step limit was reached
|
|
929
|
+
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
930
|
+
* abort wearing the cap message).
|
|
931
|
+
*
|
|
932
|
+
* Takes plain booleans rather than a turn clock so a loop whose wall-clock
|
|
933
|
+
* deadline is owned by something other than `createTurnClock` — AI Studio's
|
|
934
|
+
* pre-existing per-request timeout controller — can report through the same
|
|
935
|
+
* builders instead of growing a second set.
|
|
936
|
+
*/
|
|
937
|
+
export function buildLoopExitMessage(params) {
|
|
938
|
+
if (params.timedOut) {
|
|
939
|
+
return buildTurnTimeoutMessage(params.elapsedMs, params.toolCallCount);
|
|
940
|
+
}
|
|
941
|
+
if (params.stalled) {
|
|
942
|
+
return buildTurnStalledMessage(params.stallTimeoutMs ?? 0, params.toolCallCount);
|
|
943
|
+
}
|
|
944
|
+
if (params.wasAborted) {
|
|
945
|
+
return buildAbortedTurnMessage(params.toolCallCount);
|
|
946
|
+
}
|
|
947
|
+
return buildToolLoopCapMessage(params.maxSteps, params.toolCallCount);
|
|
948
|
+
}
|
|
925
949
|
/**
|
|
926
950
|
* Wrap-up nudge injected when the remaining turn time drops inside the
|
|
927
951
|
* `wrapupTimeLeadMs` window — the time-budget twin of the soft step-budget
|
|
@@ -300,18 +300,9 @@ export declare class GoogleVertexProvider extends BaseProvider {
|
|
|
300
300
|
*/
|
|
301
301
|
private emitGenerationEnd;
|
|
302
302
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* malformed-call retries — are observable by consumers' own pipelines.
|
|
307
|
-
*/
|
|
308
|
-
private emitTurnEvent;
|
|
309
|
-
/**
|
|
310
|
-
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
311
|
-
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
312
|
-
* only — time/stall/abort exits must never claim a step limit was reached
|
|
313
|
-
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
314
|
-
* abort wearing the cap message).
|
|
303
|
+
* Turn-clock-shaped adapter over the shared {@link buildLoopExitMessage}.
|
|
304
|
+
* Every call site in this file holds a clock, so the mapping lives here
|
|
305
|
+
* rather than at ten call sites; the decision itself is the shared one.
|
|
315
306
|
*/
|
|
316
307
|
private buildLoopExitMessage;
|
|
317
308
|
protected formatProviderError(error: unknown): Error;
|
|
@@ -32,7 +32,7 @@ import { convertZodToJsonSchema, inlineJsonSchema, ensureNestedSchemaTypes, } fr
|
|
|
32
32
|
import { createNativeThinkingConfig } from "../../utils/thinkingConfig.js";
|
|
33
33
|
import { TimeoutError, withTimeout } from "../../utils/async/index.js";
|
|
34
34
|
import { parseTimeout } from "../../utils/timeout.js";
|
|
35
|
-
import { appendStepText,
|
|
35
|
+
import { appendStepText, buildContextCapMessage, buildDedupedEngineTools, buildLoopExitMessage, buildToolLoopCapMessage, buildWrapupNudgeText, createContextGuard, createTurnClock, extractThoughtSignature, isAbortError, mapGeminiFinishReason, prependConversationMessages, resolveTurnStopReason, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
|
|
36
36
|
import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
|
|
37
37
|
import { runAgenticLoop } from "../../core/loopEngine.js";
|
|
38
38
|
import { createAnthropicLoopAdapter } from "../anthropic/loopAdapter.js";
|
|
@@ -5697,41 +5697,22 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5697
5697
|
}
|
|
5698
5698
|
}
|
|
5699
5699
|
/**
|
|
5700
|
-
*
|
|
5701
|
-
*
|
|
5702
|
-
*
|
|
5703
|
-
* malformed-call retries — are observable by consumers' own pipelines.
|
|
5704
|
-
*/
|
|
5705
|
-
emitTurnEvent(payload) {
|
|
5706
|
-
try {
|
|
5707
|
-
this.neurolink?.getEventEmitter()?.emit("turn:lifecycle", {
|
|
5708
|
-
provider: this.providerName,
|
|
5709
|
-
timestamp: Date.now(),
|
|
5710
|
-
...payload,
|
|
5711
|
-
});
|
|
5712
|
-
}
|
|
5713
|
-
catch {
|
|
5714
|
-
/* listener errors are non-fatal */
|
|
5715
|
-
}
|
|
5716
|
-
}
|
|
5717
|
-
/**
|
|
5718
|
-
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
5719
|
-
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
5720
|
-
* only — time/stall/abort exits must never claim a step limit was reached
|
|
5721
|
-
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
5722
|
-
* abort wearing the cap message).
|
|
5700
|
+
* Turn-clock-shaped adapter over the shared {@link buildLoopExitMessage}.
|
|
5701
|
+
* Every call site in this file holds a clock, so the mapping lives here
|
|
5702
|
+
* rather than at ten call sites; the decision itself is the shared one.
|
|
5723
5703
|
*/
|
|
5724
5704
|
buildLoopExitMessage(params) {
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5705
|
+
return buildLoopExitMessage({
|
|
5706
|
+
timedOut: params.turnClock.timedOut,
|
|
5707
|
+
stalled: params.turnClock.stalled,
|
|
5708
|
+
elapsedMs: params.turnClock.elapsedMs(),
|
|
5709
|
+
wasAborted: params.wasAborted,
|
|
5710
|
+
...(params.stallTimeoutMs !== undefined
|
|
5711
|
+
? { stallTimeoutMs: params.stallTimeoutMs }
|
|
5712
|
+
: {}),
|
|
5713
|
+
maxSteps: params.maxSteps,
|
|
5714
|
+
toolCallCount: params.toolCallCount,
|
|
5715
|
+
});
|
|
5735
5716
|
}
|
|
5736
5717
|
formatProviderError(error) {
|
|
5737
5718
|
const errorRecord = error;
|
|
@@ -37,6 +37,10 @@ export declare const buildWireToolNameMaps: (names: readonly string[], reservedW
|
|
|
37
37
|
* tool definitions). Used by the per-request max_tokens fit against a
|
|
38
38
|
* RUNTIME-DISCOVERED context window — deliberately the same char-based
|
|
39
39
|
* estimator the budget pipeline uses, so both layers agree.
|
|
40
|
+
*
|
|
41
|
+
* Image parts are billed at the flat IMAGE_TOKEN_ESTIMATE, never by the length
|
|
42
|
+
* of their base64 data URL: a PDF rendered to 10 page images otherwise
|
|
43
|
+
* stringifies to ~1.2M "tokens" and is rejected before it is ever sent.
|
|
40
44
|
*/
|
|
41
45
|
export declare const estimateWireTokens: (messages: ReadonlyArray<OpenAICompatChatMessage>, tools: OpenAICompatChatTool[] | undefined, provider?: string) => number;
|
|
42
46
|
export declare const safeStringify: (value: unknown) => string;
|
|
@@ -17,7 +17,7 @@ import { createParser } from "eventsource-parser";
|
|
|
17
17
|
import { sanitizeToolName } from "../mcp/toolConverter.js";
|
|
18
18
|
import { resolveSamplingParams } from "../models/modelRegistry.js";
|
|
19
19
|
import { convertZodToJsonSchema, normalizeWireToolSchema, } from "../utils/schemaConversion.js";
|
|
20
|
-
import { estimateTokens, TOKENS_PER_MESSAGE, } from "../utils/tokenEstimation.js";
|
|
20
|
+
import { estimateTokens, IMAGE_TOKEN_ESTIMATE, TOKENS_PER_MESSAGE, } from "../utils/tokenEstimation.js";
|
|
21
21
|
export const stripTrailingSlash = (s) => s.replace(/\/+$/, "");
|
|
22
22
|
// OpenAI-compatible wire tool names: first char letter/underscore (some
|
|
23
23
|
// chat templates treat a leading digit or hyphen as invalid identifiers),
|
|
@@ -68,14 +68,29 @@ export const buildWireToolNameMaps = (names, reservedWireNames) => {
|
|
|
68
68
|
* tool definitions). Used by the per-request max_tokens fit against a
|
|
69
69
|
* RUNTIME-DISCOVERED context window — deliberately the same char-based
|
|
70
70
|
* estimator the budget pipeline uses, so both layers agree.
|
|
71
|
+
*
|
|
72
|
+
* Image parts are billed at the flat IMAGE_TOKEN_ESTIMATE, never by the length
|
|
73
|
+
* of their base64 data URL: a PDF rendered to 10 page images otherwise
|
|
74
|
+
* stringifies to ~1.2M "tokens" and is rejected before it is ever sent.
|
|
71
75
|
*/
|
|
72
76
|
export const estimateWireTokens = (messages, tools, provider) => {
|
|
73
77
|
let total = 0;
|
|
74
78
|
for (const message of messages) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
if (Array.isArray(message.content)) {
|
|
80
|
+
for (const part of message.content) {
|
|
81
|
+
total +=
|
|
82
|
+
part.type === "image_url"
|
|
83
|
+
? IMAGE_TOKEN_ESTIMATE
|
|
84
|
+
: estimateTokens(part.text, provider);
|
|
85
|
+
}
|
|
86
|
+
total += TOKENS_PER_MESSAGE;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const content = typeof message.content === "string"
|
|
90
|
+
? message.content
|
|
91
|
+
: safeStringify(message.content);
|
|
92
|
+
total += estimateTokens(content, provider) + TOKENS_PER_MESSAGE;
|
|
93
|
+
}
|
|
79
94
|
// tool_calls only exists on the assistant variant of the message union.
|
|
80
95
|
const toolCalls = message.tool_calls;
|
|
81
96
|
if (toolCalls) {
|
package/dist/types/file.d.ts
CHANGED
|
@@ -552,6 +552,19 @@ export type PDFImagePage = {
|
|
|
552
552
|
/** Populated when this page failed to render (#294). */
|
|
553
553
|
error?: string;
|
|
554
554
|
};
|
|
555
|
+
/**
|
|
556
|
+
* A PDF opened for page rendering. Rendering goes through pdf-parse so the
|
|
557
|
+
* process loads exactly one pdfjs-dist copy — a second copy fails pdfjs's
|
|
558
|
+
* API-vs-Worker version check for whichever library loads second.
|
|
559
|
+
*/
|
|
560
|
+
export type PDFRenderDocument = {
|
|
561
|
+
/** Total pages in the document. */
|
|
562
|
+
length: number;
|
|
563
|
+
/** Render a 1-based page to a PNG buffer. */
|
|
564
|
+
getPage: (pageNumber: number) => Promise<Buffer>;
|
|
565
|
+
/** Release pdfjs resources held by the document. */
|
|
566
|
+
destroy: () => Promise<void>;
|
|
567
|
+
};
|
|
555
568
|
/**
|
|
556
569
|
* A single PDF queued for multimodal message building, normalised from either
|
|
557
570
|
* submission surface — `input.pdfFiles` or `input.content` with `type: "pdf"`
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* - Native PDF support for providers that accept PDF directly (Google AI, Vertex, OpenAI, Anthropic, Bedrock)
|
|
6
6
|
* - PDF → Image conversion for providers that don't support native PDF (Azure, Mistral, Ollama)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Page counting, text extraction and page rendering all go through pdf-parse,
|
|
9
|
+
* so the process only ever loads pdf-parse's pdfjs-dist. pdfjs keeps its Node
|
|
10
|
+
* worker in a process-wide global, and a second pdfjs version fails its
|
|
11
|
+
* API-vs-Worker version check against whichever copy loaded first.
|
|
9
12
|
*/
|
|
10
13
|
import type { FileProcessingResult, PDFProcessorOptions, PDFProviderConfig, PDFImageConversionOptions, PDFImageConversionResult, PDFImagePage } from "../types/index.js";
|
|
11
14
|
export declare class PDFProcessor {
|
|
@@ -40,6 +43,21 @@ export declare class PDFProcessor {
|
|
|
40
43
|
* falls back to the regex estimate.
|
|
41
44
|
*/
|
|
42
45
|
private static getAccuratePageCount;
|
|
46
|
+
/**
|
|
47
|
+
* Font and CMap directories of the pdfjs-dist copy pdf-parse itself loads,
|
|
48
|
+
* so non-embedded standard fonts and CJK text render instead of dropping
|
|
49
|
+
* out. Resolved from pdf-parse's location, never our own, to stay on that
|
|
50
|
+
* single copy. Undefined when resolution fails; rendering still works, with
|
|
51
|
+
* pdfjs falling back to its built-in font substitution.
|
|
52
|
+
*/
|
|
53
|
+
private static pdfjsAssetDirs;
|
|
54
|
+
private static resolvePdfjsAssetDirs;
|
|
55
|
+
/**
|
|
56
|
+
* Open a PDF for page-by-page PNG rendering through pdf-parse. Rejects with
|
|
57
|
+
* pdf-parse's PasswordException for encrypted PDFs (#258), which the callers
|
|
58
|
+
* map to typed errors.
|
|
59
|
+
*/
|
|
60
|
+
private static openForRendering;
|
|
43
61
|
static estimateTokens(pageCount: number, mode?: "text-only" | "visual"): number;
|
|
44
62
|
/**
|
|
45
63
|
* Estimate the largest page's rendered pixel count from the PDF's MediaBox
|
|
@@ -96,7 +114,7 @@ export declare class PDFProcessor {
|
|
|
96
114
|
*
|
|
97
115
|
* NOT a wrapper of/over {@link convertToImages}, despite the similar
|
|
98
116
|
* contract — the two are independent, parallel implementations (each does
|
|
99
|
-
* its own
|
|
117
|
+
* its own document open, downscale calculation, and page loop)
|
|
100
118
|
* rather than one delegating to the other. Keep behavior changes (page
|
|
101
119
|
* isolation, downscale, password handling) in sync across both by hand.
|
|
102
120
|
*/
|
|
@@ -113,7 +131,7 @@ export declare class PDFProcessor {
|
|
|
113
131
|
* Check if PDF to image conversion is available
|
|
114
132
|
* Useful for feature detection
|
|
115
133
|
*
|
|
116
|
-
* @returns true if pdf-
|
|
134
|
+
* @returns true if the pdf-parse renderer is available
|
|
117
135
|
*/
|
|
118
136
|
static isImageConversionAvailable(): Promise<boolean>;
|
|
119
137
|
/**
|
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
* - Native PDF support for providers that accept PDF directly (Google AI, Vertex, OpenAI, Anthropic, Bedrock)
|
|
6
6
|
* - PDF → Image conversion for providers that don't support native PDF (Azure, Mistral, Ollama)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Page counting, text extraction and page rendering all go through pdf-parse,
|
|
9
|
+
* so the process only ever loads pdf-parse's pdfjs-dist. pdfjs keeps its Node
|
|
10
|
+
* worker in a process-wide global, and a second pdfjs version fails its
|
|
11
|
+
* API-vs-Worker version check against whichever copy loaded first.
|
|
9
12
|
*/
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import path from "node:path";
|
|
10
15
|
import { PDF_LIMITS } from "../core/constants.js";
|
|
11
16
|
import { ErrorFactory } from "./errorHandling.js";
|
|
12
17
|
import { logger } from "./logger.js";
|
|
@@ -297,6 +302,76 @@ export class PDFProcessor {
|
|
|
297
302
|
return null;
|
|
298
303
|
}
|
|
299
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Font and CMap directories of the pdfjs-dist copy pdf-parse itself loads,
|
|
307
|
+
* so non-embedded standard fonts and CJK text render instead of dropping
|
|
308
|
+
* out. Resolved from pdf-parse's location, never our own, to stay on that
|
|
309
|
+
* single copy. Undefined when resolution fails; rendering still works, with
|
|
310
|
+
* pdfjs falling back to its built-in font substitution.
|
|
311
|
+
*/
|
|
312
|
+
static pdfjsAssetDirs;
|
|
313
|
+
static resolvePdfjsAssetDirs() {
|
|
314
|
+
if (PDFProcessor.pdfjsAssetDirs === undefined) {
|
|
315
|
+
try {
|
|
316
|
+
const pdfParseEntry = createRequire(import.meta.url).resolve("pdf-parse");
|
|
317
|
+
const pdfjsRoot = path.dirname(createRequire(pdfParseEntry).resolve("pdfjs-dist/package.json"));
|
|
318
|
+
PDFProcessor.pdfjsAssetDirs = {
|
|
319
|
+
standardFontDataUrl: path.join(pdfjsRoot, "standard_fonts") + path.sep,
|
|
320
|
+
cMapUrl: path.join(pdfjsRoot, "cmaps") + path.sep,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
logger.debug(`[PDF→Image] pdfjs font/CMap directories unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
PDFProcessor.pdfjsAssetDirs = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return PDFProcessor.pdfjsAssetDirs ?? undefined;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Open a PDF for page-by-page PNG rendering through pdf-parse. Rejects with
|
|
332
|
+
* pdf-parse's PasswordException for encrypted PDFs (#258), which the callers
|
|
333
|
+
* map to typed errors.
|
|
334
|
+
*/
|
|
335
|
+
static async openForRendering(pdfBuffer, scale, password) {
|
|
336
|
+
const { PDFParse } = await import("pdf-parse");
|
|
337
|
+
const parser = new PDFParse({
|
|
338
|
+
data: new Uint8Array(pdfBuffer),
|
|
339
|
+
isEvalSupported: false,
|
|
340
|
+
cMapPacked: true,
|
|
341
|
+
...PDFProcessor.resolvePdfjsAssetDirs(),
|
|
342
|
+
...(password ? { password } : {}),
|
|
343
|
+
});
|
|
344
|
+
try {
|
|
345
|
+
const { total } = await parser.getInfo();
|
|
346
|
+
return {
|
|
347
|
+
length: total,
|
|
348
|
+
getPage: async (pageNumber) => {
|
|
349
|
+
const shot = await parser.getScreenshot({
|
|
350
|
+
partial: [pageNumber],
|
|
351
|
+
scale,
|
|
352
|
+
imageBuffer: true,
|
|
353
|
+
imageDataUrl: false,
|
|
354
|
+
});
|
|
355
|
+
const page = shot.pages[0];
|
|
356
|
+
if (!page || page.data.length === 0) {
|
|
357
|
+
throw new Error(`page ${pageNumber} rendered no image data`);
|
|
358
|
+
}
|
|
359
|
+
return Buffer.from(page.data);
|
|
360
|
+
},
|
|
361
|
+
destroy: async () => {
|
|
362
|
+
await parser.destroy().catch(() => {
|
|
363
|
+
/* cleanup - ignore destroy errors */
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
await parser.destroy().catch(() => {
|
|
370
|
+
/* cleanup - ignore destroy errors */
|
|
371
|
+
});
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
300
375
|
static estimateTokens(pageCount, mode = "visual") {
|
|
301
376
|
if (mode === "text-only") {
|
|
302
377
|
return Math.ceil((pageCount / 3) * 1000);
|
|
@@ -395,16 +470,14 @@ export class PDFProcessor {
|
|
|
395
470
|
estimatedMemoryMB,
|
|
396
471
|
sizeMB: Number(sizeMB.toFixed(2)),
|
|
397
472
|
});
|
|
473
|
+
let document;
|
|
398
474
|
try {
|
|
399
|
-
// Dynamic import to avoid loading MuPDF binaries until needed
|
|
400
|
-
const pdfToImgModule = await import("pdf-to-img");
|
|
401
|
-
const pdf = pdfToImgModule.pdf;
|
|
402
475
|
logger.debug("[PDF→Image] Starting PDF to image conversion", {
|
|
403
476
|
bufferSize: pdfBuffer.length,
|
|
404
477
|
scale,
|
|
405
478
|
maxPages: maxPages || "all",
|
|
406
479
|
});
|
|
407
|
-
// #260: pre-flight page-size check WITHOUT rendering.
|
|
480
|
+
// #260: pre-flight page-size check WITHOUT rendering. The renderer applies
|
|
408
481
|
// `scale` uniformly with no per-page hook and no pixel guard, so a very
|
|
409
482
|
// large page (e.g. an architectural drawing with a huge MediaBox) can
|
|
410
483
|
// allocate gigabytes of canvas. Read the largest MediaBox from the PDF
|
|
@@ -416,7 +489,7 @@ export class PDFProcessor {
|
|
|
416
489
|
const downscale = Math.sqrt(maxCanvasPixels / largestPixels);
|
|
417
490
|
// Floor the result: an astronomically large (but now finite, see
|
|
418
491
|
// `largestPagePixels`) pixel estimate would otherwise push `downscale`
|
|
419
|
-
// — and therefore `effectiveScale` — toward 0, handing
|
|
492
|
+
// — and therefore `effectiveScale` — toward 0, handing the renderer a
|
|
420
493
|
// degenerate viewport instead of a small-but-renderable page.
|
|
421
494
|
effectiveScale = Math.max(PDF_LIMITS.MIN_EFFECTIVE_SCALE, scale * downscale);
|
|
422
495
|
// Recompute the ratio actually applied (may differ from `downscale`
|
|
@@ -431,14 +504,9 @@ export class PDFProcessor {
|
|
|
431
504
|
logger.warn(`[PDF→Image] ⚠️ ${msg}`);
|
|
432
505
|
warnings.push(msg);
|
|
433
506
|
}
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
// iterator — that lets one bad page be isolated instead of aborting all.
|
|
438
|
-
const document = await pdf(pdfBuffer, {
|
|
439
|
-
scale: effectiveScale,
|
|
440
|
-
...(password ? { password } : {}),
|
|
441
|
-
});
|
|
507
|
+
// Password forwarded for encrypted PDFs (#258). An indexed getPage()
|
|
508
|
+
// loop lets one bad page be isolated instead of aborting all.
|
|
509
|
+
document = await PDFProcessor.openForRendering(pdfBuffer, effectiveScale, password);
|
|
442
510
|
const totalPages = document.length;
|
|
443
511
|
// #294: convert page-by-page with per-page isolation. A single page whose
|
|
444
512
|
// canvas render throws (e.g. a degenerate MediaBox) no longer discards
|
|
@@ -518,6 +586,9 @@ export class PDFProcessor {
|
|
|
518
586
|
cause: error,
|
|
519
587
|
});
|
|
520
588
|
}
|
|
589
|
+
finally {
|
|
590
|
+
await document?.destroy();
|
|
591
|
+
}
|
|
521
592
|
}
|
|
522
593
|
/**
|
|
523
594
|
* Shared input validation for the image-conversion paths. Returns the PDF
|
|
@@ -568,7 +639,7 @@ export class PDFProcessor {
|
|
|
568
639
|
*
|
|
569
640
|
* NOT a wrapper of/over {@link convertToImages}, despite the similar
|
|
570
641
|
* contract — the two are independent, parallel implementations (each does
|
|
571
|
-
* its own
|
|
642
|
+
* its own document open, downscale calculation, and page loop)
|
|
572
643
|
* rather than one delegating to the other. Keep behavior changes (page
|
|
573
644
|
* isolation, downscale, password handling) in sync across both by hand.
|
|
574
645
|
*/
|
|
@@ -581,8 +652,6 @@ export class PDFProcessor {
|
|
|
581
652
|
maxCanvasPixels,
|
|
582
653
|
maxPages,
|
|
583
654
|
});
|
|
584
|
-
const pdfToImgModule = await import("pdf-to-img");
|
|
585
|
-
const pdf = pdfToImgModule.pdf;
|
|
586
655
|
// #260: uniform downscale so the largest page stays under maxCanvasPixels.
|
|
587
656
|
let effectiveScale = scale;
|
|
588
657
|
const largestPixels = PDFProcessor.largestPagePixels(pdfBuffer, scale);
|
|
@@ -596,10 +665,7 @@ export class PDFProcessor {
|
|
|
596
665
|
// being yielded as a page error.
|
|
597
666
|
let document;
|
|
598
667
|
try {
|
|
599
|
-
document = await
|
|
600
|
-
scale: effectiveScale,
|
|
601
|
-
...(password ? { password } : {}),
|
|
602
|
-
});
|
|
668
|
+
document = await PDFProcessor.openForRendering(pdfBuffer, effectiveScale, password);
|
|
603
669
|
}
|
|
604
670
|
catch (error) {
|
|
605
671
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -617,33 +683,43 @@ export class PDFProcessor {
|
|
|
617
683
|
}
|
|
618
684
|
const totalPages = document.length;
|
|
619
685
|
let converted = 0;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
686
|
+
try {
|
|
687
|
+
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
|
|
688
|
+
if (maxPages !== undefined && pageNum - 1 >= maxPages) {
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
const page = await document.getPage(pageNum);
|
|
693
|
+
const base64Image = page.toString("base64");
|
|
694
|
+
converted++;
|
|
695
|
+
if (onProgress) {
|
|
696
|
+
await onProgress({
|
|
697
|
+
pagesConverted: converted,
|
|
698
|
+
totalPages,
|
|
699
|
+
elapsedMs: Date.now() - startTime,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
yield {
|
|
703
|
+
pageIndex: pageNum,
|
|
704
|
+
image: base64Image,
|
|
705
|
+
imageSizeBytes: page.length,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
catch (pageError) {
|
|
709
|
+
const msg = pageError instanceof Error ? pageError.message : String(pageError);
|
|
710
|
+
logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render (stream): ${msg}`);
|
|
711
|
+
yield {
|
|
712
|
+
pageIndex: pageNum,
|
|
713
|
+
image: "",
|
|
714
|
+
imageSizeBytes: 0,
|
|
715
|
+
error: msg,
|
|
716
|
+
};
|
|
634
717
|
}
|
|
635
|
-
yield {
|
|
636
|
-
pageIndex: pageNum,
|
|
637
|
-
image: base64Image,
|
|
638
|
-
imageSizeBytes: page.length,
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
catch (pageError) {
|
|
642
|
-
const msg = pageError instanceof Error ? pageError.message : String(pageError);
|
|
643
|
-
logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render (stream): ${msg}`);
|
|
644
|
-
yield { pageIndex: pageNum, image: "", imageSizeBytes: 0, error: msg };
|
|
645
718
|
}
|
|
646
719
|
}
|
|
720
|
+
finally {
|
|
721
|
+
await document.destroy();
|
|
722
|
+
}
|
|
647
723
|
}
|
|
648
724
|
/**
|
|
649
725
|
* Convert a PDF file path to an array of base64 PNG images
|
|
@@ -661,11 +737,11 @@ export class PDFProcessor {
|
|
|
661
737
|
* Check if PDF to image conversion is available
|
|
662
738
|
* Useful for feature detection
|
|
663
739
|
*
|
|
664
|
-
* @returns true if pdf-
|
|
740
|
+
* @returns true if the pdf-parse renderer is available
|
|
665
741
|
*/
|
|
666
742
|
static async isImageConversionAvailable() {
|
|
667
743
|
try {
|
|
668
|
-
await import("pdf-
|
|
744
|
+
await import("pdf-parse");
|
|
669
745
|
return true;
|
|
670
746
|
}
|
|
671
747
|
catch {
|
|
@@ -683,7 +759,7 @@ export class PDFProcessor {
|
|
|
683
759
|
static estimateConversionMemoryUsage(pdfSizeBytes, pageCount, scale = PDF_LIMITS.DEFAULT_SCALE) {
|
|
684
760
|
// Rough estimation:
|
|
685
761
|
// - Each page at scale 2 produces ~1-3MB PNG
|
|
686
|
-
// -
|
|
762
|
+
// - pdfjs needs ~2x PDF size for processing
|
|
687
763
|
// - Output images need ~2MB per page on average
|
|
688
764
|
const pdfProcessingMB = (pdfSizeBytes / (1024 * 1024)) * 2;
|
|
689
765
|
const outputImagesMB = pageCount * 2 * scale;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared string-distance helpers for "did you mean?" suggestions.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from toolCallRepair.ts, which had the only implementation, so
|
|
5
|
+
* provider-name validation can reuse it rather than carry a second copy.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Longest string the distance primitive will compare.
|
|
9
|
+
*
|
|
10
|
+
* Exported so every call site can apply the same limit *before* calling
|
|
11
|
+
* `levenshtein` — skipping an over-limit operand entirely, rather than
|
|
12
|
+
* comparing a truncated prefix of it. Truncating silently merged any two
|
|
13
|
+
* strings that happened to share their first `MAX_COMPARABLE_LENGTH`
|
|
14
|
+
* characters into distance 0, which let a direct MCP tool-name lookup
|
|
15
|
+
* (`resolveToolName`) resolve to the wrong tool for a long, colliding name.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_COMPARABLE_LENGTH = 128;
|
|
18
|
+
/**
|
|
19
|
+
* Compute Levenshtein edit distance between two strings.
|
|
20
|
+
* Uses the iterative matrix approach — O(m*n) time, O(min(m,n)) space.
|
|
21
|
+
*
|
|
22
|
+
* Throws rather than truncating when either input exceeds
|
|
23
|
+
* `MAX_COMPARABLE_LENGTH`: silently comparing truncated prefixes produces a
|
|
24
|
+
* *wrong* distance (two distinct strings sharing a long prefix collapse to
|
|
25
|
+
* distance 0) instead of no distance, which is worse than refusing outright.
|
|
26
|
+
* Callers that want fuzzy matching over long input must apply the length
|
|
27
|
+
* check themselves and skip the comparison — see `suggestClosest` below and
|
|
28
|
+
* `resolveToolName`/`rankToolNameCandidates` in toolCallRepair.ts.
|
|
29
|
+
*/
|
|
30
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
31
|
+
/**
|
|
32
|
+
* Candidates within `maxDistance` edits of `input`, closest first.
|
|
33
|
+
*
|
|
34
|
+
* Ties break on the candidate's own order in `candidates`, so a caller that
|
|
35
|
+
* passes canonical names before aliases gets the canonical one suggested.
|
|
36
|
+
* Comparison is case-insensitive; the returned strings keep their original
|
|
37
|
+
* casing so they can be pasted back verbatim.
|
|
38
|
+
*
|
|
39
|
+
* `input` longer than `MAX_COMPARABLE_LENGTH` is rejected outright (no
|
|
40
|
+
* suggestions), and any individual candidate over the same limit is skipped
|
|
41
|
+
* rather than compared — never truncated. Truncating either side risks
|
|
42
|
+
* exactly the false distance-0 match `levenshtein`'s guard rejects: two
|
|
43
|
+
* distinct strings sharing a long-enough prefix would otherwise look
|
|
44
|
+
* identical and "suggest" the wrong one.
|
|
45
|
+
*
|
|
46
|
+
* @param input - The string the user actually typed
|
|
47
|
+
* @param candidates - Valid values, most-preferred first
|
|
48
|
+
* @param options.maxDistance - Edit budget (default 3)
|
|
49
|
+
* @param options.limit - Most suggestions to return (default 3)
|
|
50
|
+
*/
|
|
51
|
+
export declare function suggestClosest(input: string, candidates: readonly string[], options?: {
|
|
52
|
+
maxDistance?: number;
|
|
53
|
+
limit?: number;
|
|
54
|
+
}): string[];
|