@juspay/neurolink 12.17.2 → 12.17.4
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 -2
- package/dist/browser/neurolink.min.js +408 -408
- package/dist/neurolink.js +35 -5
- package/dist/providers/googleAiStudio/client.js +10 -0
- package/dist/providers/openaiChatCompletionsBase.js +60 -51
- package/dist/utils/fileDetector.js +28 -7
- package/dist/utils/messageBuilder.js +51 -26
- package/dist/utils/pdfProcessor.js +26 -4
- package/package.json +6 -2
package/dist/neurolink.js
CHANGED
|
@@ -4470,6 +4470,14 @@ Current user's request: ${currentInput}`;
|
|
|
4470
4470
|
// remains false for backwards-compat with existing listeners that only
|
|
4471
4471
|
// branch on the boolean.
|
|
4472
4472
|
const aborted = isAbortError(error);
|
|
4473
|
+
// A native path that already emitted its own `generation:end` for this
|
|
4474
|
+
// failure marks the error, because on the failure path it has no result
|
|
4475
|
+
// object to mark. Re-emitting here would double every native failure.
|
|
4476
|
+
if (error &&
|
|
4477
|
+
typeof error === "object" &&
|
|
4478
|
+
error._generationEndEmitted) {
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4473
4481
|
try {
|
|
4474
4482
|
this.emitter.emit("generation:end", {
|
|
4475
4483
|
provider: errProvider,
|
|
@@ -5899,7 +5907,10 @@ Current user's request: ${currentInput}`;
|
|
|
5899
5907
|
toolsUsed: result.toolsUsed || [],
|
|
5900
5908
|
toolCalls: result.toolCalls ?? [],
|
|
5901
5909
|
toolExecutions: transformedToolExecutions,
|
|
5902
|
-
|
|
5910
|
+
// A tool RAN (toolsUsed), not merely dispatched (toolExecutions can
|
|
5911
|
+
// include failed/never-found calls) — see hasToolExecutions above,
|
|
5912
|
+
// which still gates the null-result check and must not change.
|
|
5913
|
+
enhancedWithTools: !!(result.toolsUsed && result.toolsUsed.length > 0),
|
|
5903
5914
|
availableTools: transformToolsForMCP(transformToolsToExpectedFormat(availableTools)),
|
|
5904
5915
|
audio: result.audio,
|
|
5905
5916
|
ttsMetadata: result.ttsMetadata,
|
|
@@ -6060,7 +6071,8 @@ Current user's request: ${currentInput}`;
|
|
|
6060
6071
|
executionTime: te.durationMs,
|
|
6061
6072
|
success: !te.isError,
|
|
6062
6073
|
})),
|
|
6063
|
-
|
|
6074
|
+
// A tool RAN (toolsUsed), not merely dispatched.
|
|
6075
|
+
enhancedWithTools: !!poolResult.toolsUsed?.length,
|
|
6064
6076
|
analytics: poolResult.analytics,
|
|
6065
6077
|
evaluation: poolResult.evaluation,
|
|
6066
6078
|
audio: poolResult.audio,
|
|
@@ -6432,7 +6444,8 @@ Current user's request: ${currentInput}`;
|
|
|
6432
6444
|
executionTime: te.durationMs,
|
|
6433
6445
|
success: !te.isError,
|
|
6434
6446
|
})),
|
|
6435
|
-
|
|
6447
|
+
// A tool RAN (toolsUsed), not merely dispatched.
|
|
6448
|
+
enhancedWithTools: !!result.toolsUsed?.length,
|
|
6436
6449
|
analytics: result.analytics,
|
|
6437
6450
|
evaluation: result.evaluation,
|
|
6438
6451
|
audio: result.audio,
|
|
@@ -6496,10 +6509,27 @@ Current user's request: ${currentInput}`;
|
|
|
6496
6509
|
// naming the one provider that failed is clearer than the generic
|
|
6497
6510
|
// multi-provider wording. The multi-provider prefix is left untouched
|
|
6498
6511
|
// so existing message-matching suites keep passing.
|
|
6512
|
+
// Both wrappers replace the provider's error with a new one, discarding
|
|
6513
|
+
// anything attached to it. `_generationEndEmitted` has to survive: lose it
|
|
6514
|
+
// and emitGenerateErrorEvent emits a second event for a failure the native
|
|
6515
|
+
// provider already reported. Returns the error rather than throwing so each
|
|
6516
|
+
// call site keeps its own `throw` — a never-returning helper reads better
|
|
6517
|
+
// but tsconfig.cli.json will not accept it as terminating the enclosing
|
|
6518
|
+
// function (TS2366). `cause` is set because the original error and its
|
|
6519
|
+
// stack were simply being thrown away.
|
|
6520
|
+
const wrapProviderError = (message) => {
|
|
6521
|
+
const wrapped = new Error(message, { cause: lastError });
|
|
6522
|
+
if (lastError &&
|
|
6523
|
+
lastError._generationEndEmitted) {
|
|
6524
|
+
wrapped._generationEndEmitted =
|
|
6525
|
+
true;
|
|
6526
|
+
}
|
|
6527
|
+
return wrapped;
|
|
6528
|
+
};
|
|
6499
6529
|
if (providersToTry.length === 1) {
|
|
6500
|
-
throw
|
|
6530
|
+
throw wrapProviderError(`Provider ${providersToTry[0]} failed: ${lastError?.message || "Unknown error"}`);
|
|
6501
6531
|
}
|
|
6502
|
-
throw
|
|
6532
|
+
throw wrapProviderError(`Failed to generate text with all providers. Last error: ${lastError?.message || "Unknown error"}`);
|
|
6503
6533
|
}
|
|
6504
6534
|
/**
|
|
6505
6535
|
* Create tool-aware system prompt that informs AI about available tools
|
|
@@ -1364,6 +1364,16 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
1364
1364
|
result._generationEndEmitted =
|
|
1365
1365
|
true;
|
|
1366
1366
|
}
|
|
1367
|
+
// The failure path has no result to carry that flag — `result` is null —
|
|
1368
|
+
// so mark the error instead. Without this the provider's own
|
|
1369
|
+
// `generation:end` (success: false) and the SDK's emitGenerateErrorEvent
|
|
1370
|
+
// both fire for one failed native generate, double-counting every failure
|
|
1371
|
+
// in analytics and as two Pipeline B spans. The success path above was
|
|
1372
|
+
// already deduplicated; only the catch branch was missed.
|
|
1373
|
+
if (!success && error && typeof error === "object") {
|
|
1374
|
+
error._generationEndEmitted =
|
|
1375
|
+
true;
|
|
1376
|
+
}
|
|
1367
1377
|
emitter.emit("generation:end", {
|
|
1368
1378
|
provider: this.providerName,
|
|
1369
1379
|
responseTime: Date.now() - startTime,
|
|
@@ -899,57 +899,66 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
899
899
|
// Omit the tools for a pass that only has to reformat an answer the
|
|
900
900
|
// model has already given. Dropping them is what makes `response_format`
|
|
901
901
|
// legal again on a vendor that refuses the two together.
|
|
902
|
-
withTools = true) =>
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
? {
|
|
944
|
-
:
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
902
|
+
withTools = true) => {
|
|
903
|
+
return runNativeGenerateLoop({
|
|
904
|
+
doGenerate,
|
|
905
|
+
...createNativeGenerateGuard({
|
|
906
|
+
provider: this.providerName,
|
|
907
|
+
availableInputTokens: getAvailableInputTokens(this.providerName, modelId, options.maxTokens ?? undefined),
|
|
908
|
+
// Runs on every step of the generate loop, so a cyclic or
|
|
909
|
+
// oversized tool schema must not throw the turn away. Shares the
|
|
910
|
+
// estimator's own serializer: its fallback over-reports, which
|
|
911
|
+
// makes the guard fire rather than silently size the overhead at
|
|
912
|
+
// nothing.
|
|
913
|
+
// Charge the guard only for what this pass actually sends. A
|
|
914
|
+
// reformat pass runs with `withTools === false` and omits `tools`
|
|
915
|
+
// from the request (see the spread below), so counting them here
|
|
916
|
+
// over-reports the fixed overhead. That inflation is not cosmetic:
|
|
917
|
+
// it feeds `planLoopGuardReclaim`, so a large tool schema can push
|
|
918
|
+
// the estimate past the reclaim threshold and drop older tool
|
|
919
|
+
// exchanges out of a reformat request that would have fit.
|
|
920
|
+
getFixedOverheadTokens: () => estimateTokens(serializeForEstimate({
|
|
921
|
+
...(withTools && v3Tools ? { tools: v3Tools } : {}),
|
|
922
|
+
responseFormat: format,
|
|
923
|
+
}), this.providerName),
|
|
924
|
+
}),
|
|
925
|
+
conversation: conv,
|
|
926
|
+
...(withTools && v3Tools ? { tools: v3Tools } : {}),
|
|
927
|
+
toolsRecord,
|
|
928
|
+
...(withTools && hasTools && options.toolChoice
|
|
929
|
+
? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
|
|
930
|
+
: {}),
|
|
931
|
+
// The per-call `timeout` keeps its per-MODEL-CALL meaning once
|
|
932
|
+
// `turnTimeoutMs` owns the whole-turn deadline, and it reaches the
|
|
933
|
+
// model layer only through this channel. Without it each step fell
|
|
934
|
+
// back to the provider default, so a caller asking for a short
|
|
935
|
+
// per-call timeout got the default on every request.
|
|
936
|
+
...(typeof options.timeout === "number"
|
|
937
|
+
? {
|
|
938
|
+
providerOptions: {
|
|
939
|
+
neurolink: { timeoutMs: options.timeout },
|
|
940
|
+
},
|
|
941
|
+
}
|
|
942
|
+
: {}),
|
|
943
|
+
...(format ? { responseFormat: format } : {}),
|
|
944
|
+
maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
|
|
945
|
+
...(options.maxTokens ? { maxOutputTokens: options.maxTokens } : {}),
|
|
946
|
+
...(options.temperature !== undefined
|
|
947
|
+
? { temperature: options.temperature }
|
|
948
|
+
: {}),
|
|
949
|
+
...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
|
|
950
|
+
...(options.toolTimeoutMs !== undefined
|
|
951
|
+
? { toolTimeoutMs: options.toolTimeoutMs }
|
|
952
|
+
: {}),
|
|
953
|
+
// withProviderRetry + handleProviderError are what the ai loop
|
|
954
|
+
// supplied around each call: without them a 429 surfaces as a raw
|
|
955
|
+
// upstream string instead of a RateLimitError, and a throttle is
|
|
956
|
+
// never retried.
|
|
957
|
+
runStep: (call) => withProviderRetry(call, trace.getActiveSpan() ?? undefined, `${this.providerName} generate`).catch((err) => {
|
|
958
|
+
throw this.handleProviderError(err);
|
|
959
|
+
}),
|
|
960
|
+
}, toolExecutionSummaries);
|
|
961
|
+
};
|
|
953
962
|
// Structured output rides `response_format` first. When a vendor rejects
|
|
954
963
|
// that outright — a tools/JSON-mode conflict, or a schema its constrained
|
|
955
964
|
// decoder will not accept — the recovery is to ask for the same object in
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Uses multi-strategy approach for reliable type identification
|
|
5
5
|
*/
|
|
6
6
|
import { open, readFile, realpath } from "fs/promises";
|
|
7
|
+
import { STATUS_CODES } from "node:http";
|
|
7
8
|
import { basename, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
|
|
8
9
|
import { request } from "undici";
|
|
9
10
|
import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
|
|
@@ -675,21 +676,35 @@ export class FileDetector {
|
|
|
675
676
|
new ContentHeuristicStrategy(),
|
|
676
677
|
];
|
|
677
678
|
let best = null;
|
|
679
|
+
// #397: track which strategy produced `best` so the eventual log line
|
|
680
|
+
// can name it — the strategy instances themselves are gone by the time
|
|
681
|
+
// logging happens below, only their results survive.
|
|
682
|
+
let bestStrategyName = "none";
|
|
678
683
|
for (const strategy of strategies) {
|
|
684
|
+
const strategyName = strategy.constructor.name;
|
|
679
685
|
const result = await strategy.detect(strategy instanceof ExtensionStrategy && extensionInput
|
|
680
686
|
? extensionInput
|
|
681
687
|
: input);
|
|
682
688
|
if (!best || result.metadata.confidence > best.metadata.confidence) {
|
|
683
689
|
best = result;
|
|
690
|
+
bestStrategyName = strategyName;
|
|
684
691
|
}
|
|
685
692
|
if (result.metadata.confidence >= confidenceThreshold) {
|
|
686
|
-
logger.info(`[FileDetector] Type: ${result.type} (${result.metadata.confidence}
|
|
693
|
+
logger.info(`[FileDetector] Type: ${result.type} (${result.metadata.confidence}%, strategy: ${strategyName})`);
|
|
687
694
|
return FileDetector.withResolvedExtension(result, input, options);
|
|
688
695
|
}
|
|
689
696
|
}
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
|
|
697
|
+
// A genuinely unidentifiable input — every strategy failed to produce
|
|
698
|
+
// even a type — is a real failure, not a best-effort guess, so it is
|
|
699
|
+
// surfaced at error level instead of being buried in debug output.
|
|
700
|
+
if (!best || best.type === "unknown") {
|
|
701
|
+
logger.error(`[FileDetector] Detection failed: no strategy identified a type (best: ${bestStrategyName}, ${best?.metadata.confidence ?? 0}%, threshold ${confidenceThreshold}%)`);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
// Below-threshold detection is the common case for any file under the
|
|
705
|
+
// ContentHeuristic ceiling — a debug detail, not a warning-worthy anomaly.
|
|
706
|
+
logger.debug(`[FileDetector] Best-effort type below threshold: ${best.type} (${best.metadata.confidence}%, threshold ${confidenceThreshold}%, strategy: ${bestStrategyName})`);
|
|
707
|
+
}
|
|
693
708
|
return FileDetector.withResolvedExtension(best, input, options);
|
|
694
709
|
}
|
|
695
710
|
/**
|
|
@@ -1543,9 +1558,15 @@ export class FileDetector {
|
|
|
1543
1558
|
bodyTimeout: timeout,
|
|
1544
1559
|
});
|
|
1545
1560
|
if (response.statusCode !== 200) {
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
|
|
1561
|
+
// #360: pair the code with its standard reason phrase (e.g. 404 →
|
|
1562
|
+
// "Not Found") so the message is readable rather than a bare
|
|
1563
|
+
// number. Sourced from Node's own `http.STATUS_CODES` table
|
|
1564
|
+
// rather than a hand-written map that could drift from it, and
|
|
1565
|
+
// falls back gracefully for a non-standard code that table
|
|
1566
|
+
// doesn't recognize. Query string / fragment stays stripped — a
|
|
1567
|
+
// presigned URL's token must not be echoed into a thrown error.
|
|
1568
|
+
const statusText = STATUS_CODES[response.statusCode];
|
|
1569
|
+
throw new Error(`HTTP ${response.statusCode}${statusText ? ` ${statusText}` : ""} fetching ${redactUrlForError(url)}`);
|
|
1549
1570
|
}
|
|
1550
1571
|
const contentType = response.headers["content-type"] || "";
|
|
1551
1572
|
const declaredLength = Number(response.headers["content-length"]);
|
|
@@ -1877,10 +1877,14 @@ async function processImageToBase64(image, index) {
|
|
|
1877
1877
|
const match = image.match(/^data:([^;]+);base64,(.+)$/);
|
|
1878
1878
|
if (match) {
|
|
1879
1879
|
const declaredMime = match[1];
|
|
1880
|
-
// #348:
|
|
1881
|
-
//
|
|
1882
|
-
|
|
1883
|
-
|
|
1880
|
+
// #348: validate against the actual supported-image allowlist, not
|
|
1881
|
+
// just the "image/*" prefix — "image/invalid-format" started with
|
|
1882
|
+
// "image/" and passed here, only to fail later at the provider.
|
|
1883
|
+
// Reuses ImageProcessor's allowlist (itself sourced from
|
|
1884
|
+
// imageFormatSupport's SUPPORTED_INPUT_IMAGE_MIME_TYPES) so intake
|
|
1885
|
+
// validation can never drift from the set NeuroLink actually accepts.
|
|
1886
|
+
if (!ImageProcessor.validateImageFormat(declaredMime)) {
|
|
1887
|
+
throw new Error(`Unsupported data URI MIME type for image input at index ${index}: "${declaredMime}" (not a supported image format)`);
|
|
1884
1888
|
}
|
|
1885
1889
|
mimeType = declaredMime;
|
|
1886
1890
|
imageData = match[2]; // Raw base64 only — NOT the full data: URI
|
|
@@ -2323,7 +2327,16 @@ pdfFiles, provider, model, audioFiles = []) {
|
|
|
2323
2327
|
for (const pdf of pdfFiles) {
|
|
2324
2328
|
try {
|
|
2325
2329
|
const effectiveMaxPages = pdf.maxPages ?? PDF_LIMITS.DEFAULT_MAX_PAGES;
|
|
2326
|
-
|
|
2330
|
+
// #302: stream pages one at a time instead of materialising every
|
|
2331
|
+
// rendered page before any of them reaches `content` — this is the
|
|
2332
|
+
// path generate()/stream() callers actually exercise, so it must
|
|
2333
|
+
// carry the memory characteristic convertToImagesStream was built
|
|
2334
|
+
// for. Per-page ordering, error isolation (#294) and the resulting
|
|
2335
|
+
// message content are kept identical to the batch path below;
|
|
2336
|
+
// only how the pages are produced changes.
|
|
2337
|
+
const pageErrors = [];
|
|
2338
|
+
let convertedCount = 0;
|
|
2339
|
+
for await (const page of PDFImageConverter.convertToImagesStream(pdf.buffer, {
|
|
2327
2340
|
// #297: this is the only PDF→image call the product actually makes,
|
|
2328
2341
|
// and it used to hardcode scale 2.0 — silently overriding the
|
|
2329
2342
|
// lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
|
|
@@ -2337,7 +2350,34 @@ pdfFiles, provider, model, audioFiles = []) {
|
|
|
2337
2350
|
...(pdf.maxCanvasPixels
|
|
2338
2351
|
? { maxCanvasPixels: pdf.maxCanvasPixels }
|
|
2339
2352
|
: {}), // #260
|
|
2340
|
-
})
|
|
2353
|
+
})) {
|
|
2354
|
+
if (page.error) {
|
|
2355
|
+
// #294: per-page isolation — a failed page is recorded and
|
|
2356
|
+
// skipped, not thrown, so the rest of the document still
|
|
2357
|
+
// reaches the model.
|
|
2358
|
+
pageErrors.push({ page: page.pageIndex, error: page.error });
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
convertedCount++;
|
|
2362
|
+
// Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
|
|
2363
|
+
content.push({
|
|
2364
|
+
type: "image",
|
|
2365
|
+
image: page.image,
|
|
2366
|
+
mimeType: "image/png",
|
|
2367
|
+
});
|
|
2368
|
+
logger.debug(`[PDF→Image] Added page ${page.pageIndex} of ${pdf.filename}`);
|
|
2369
|
+
}
|
|
2370
|
+
// Batch's convertToImages throws when nothing converted (either a
|
|
2371
|
+
// zero-page PDF or every page failed) instead of returning an empty
|
|
2372
|
+
// result; convertToImagesStream has no such guard (it simply yields
|
|
2373
|
+
// nothing / all-error pages), so replicate that contract explicitly
|
|
2374
|
+
// to keep the two paths' failure behavior identical.
|
|
2375
|
+
if (convertedCount === 0) {
|
|
2376
|
+
if (pageErrors.length > 0) {
|
|
2377
|
+
throw new Error(`All ${pageErrors.length} page(s) failed to render. First error: ${pageErrors[0].error}`);
|
|
2378
|
+
}
|
|
2379
|
+
throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
|
|
2380
|
+
}
|
|
2341
2381
|
// The renderer stops at maxPages, so a longer document is silently
|
|
2342
2382
|
// truncated — say so rather than letting the model answer from a
|
|
2343
2383
|
// partial document as though it had the whole thing.
|
|
@@ -2348,31 +2388,16 @@ pdfFiles, provider, model, audioFiles = []) {
|
|
|
2348
2388
|
// there. Reaching the cap is also unambiguous — a short count caused by
|
|
2349
2389
|
// per-page render failures (#294 isolates those into `errors`) would
|
|
2350
2390
|
// otherwise be misreported as a maxPages truncation.
|
|
2351
|
-
if (
|
|
2391
|
+
if (convertedCount >= effectiveMaxPages) {
|
|
2352
2392
|
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)} hit the ${effectiveMaxPages}-page ` +
|
|
2353
2393
|
`conversion limit. Any pages beyond that were not sent — the model may be ` +
|
|
2354
2394
|
`answering from a partial document. Raise pdfOptions.maxPages or split the file.`);
|
|
2355
2395
|
}
|
|
2356
|
-
if (
|
|
2357
|
-
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${
|
|
2358
|
-
`failed to render and were omitted (page ${
|
|
2359
|
-
}
|
|
2360
|
-
logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
|
|
2361
|
-
// Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
|
|
2362
|
-
conversionResult.images.forEach((base64Image, pageIndex) => {
|
|
2363
|
-
content.push({
|
|
2364
|
-
type: "image",
|
|
2365
|
-
image: base64Image,
|
|
2366
|
-
mimeType: "image/png",
|
|
2367
|
-
});
|
|
2368
|
-
logger.debug(`[PDF→Image] Added page ${pageIndex + 1}/${conversionResult.pageCount} of ${pdf.filename}`);
|
|
2369
|
-
});
|
|
2370
|
-
// Log any warnings from conversion
|
|
2371
|
-
if (conversionResult.warnings) {
|
|
2372
|
-
conversionResult.warnings.forEach((warning) => {
|
|
2373
|
-
logger.warn(`[PDF→Image] ${warning}`);
|
|
2374
|
-
});
|
|
2396
|
+
if (pageErrors.length > 0) {
|
|
2397
|
+
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${pageErrors.length} page(s) ` +
|
|
2398
|
+
`failed to render and were omitted (page ${pageErrors.map((e) => e.page).join(", ")}).`);
|
|
2375
2399
|
}
|
|
2400
|
+
logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${convertedCount} page(s) → images`);
|
|
2376
2401
|
}
|
|
2377
2402
|
catch (error) {
|
|
2378
2403
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -589,10 +589,32 @@ export class PDFProcessor {
|
|
|
589
589
|
if (largestPixels > maxCanvasPixels) {
|
|
590
590
|
effectiveScale = scale * Math.sqrt(maxCanvasPixels / largestPixels);
|
|
591
591
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
592
|
+
// #258: map pdfjs's PasswordException to the same actionable typed error
|
|
593
|
+
// convertToImages raises — kept in sync per the class docstring above.
|
|
594
|
+
// Unlike the per-page loop below, a failure here happens before any page
|
|
595
|
+
// can be isolated, so it must reject the generator outright rather than
|
|
596
|
+
// being yielded as a page error.
|
|
597
|
+
let document;
|
|
598
|
+
try {
|
|
599
|
+
document = await pdf(pdfBuffer, {
|
|
600
|
+
scale: effectiveScale,
|
|
601
|
+
...(password ? { password } : {}),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
606
|
+
const pdfErr = error;
|
|
607
|
+
if (pdfErr?.name === "PasswordException" ||
|
|
608
|
+
/password/i.test(errorMessage)) {
|
|
609
|
+
const incorrect = pdfErr.code === 2 || /incorrect|invalid/i.test(errorMessage);
|
|
610
|
+
throw incorrect
|
|
611
|
+
? ErrorFactory.pdfIncorrectPassword()
|
|
612
|
+
: ErrorFactory.pdfPasswordRequired();
|
|
613
|
+
}
|
|
614
|
+
throw new Error(`PDF to image conversion failed: ${errorMessage}`, {
|
|
615
|
+
cause: error,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
596
618
|
const totalPages = document.length;
|
|
597
619
|
let converted = 0;
|
|
598
620
|
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.17.
|
|
3
|
+
"version": "12.17.4",
|
|
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": {
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"test:context": "pnpm exec tsx test/continuous-test-suite-context.ts",
|
|
81
81
|
"test:evaluation": "pnpm exec tsx test/continuous-test-suite-evaluation.ts",
|
|
82
82
|
"test:handler-registry": "pnpm exec tsx test/continuous-test-suite-handler-registry.ts",
|
|
83
|
+
"test:harness-offline-timeout": "pnpm exec tsx test/continuous-test-suite-harness-offline-timeout.ts",
|
|
83
84
|
"test:resolve-request-kind": "pnpm exec tsx test/continuous-test-suite-resolve-request-kind.ts",
|
|
84
85
|
"test:mcp": "pnpm exec tsx test/continuous-test-suite-mcp-infra.ts",
|
|
85
86
|
"test:tool-resolution": "pnpm exec tsx test/continuous-test-suite-tool-resolution.ts",
|
|
@@ -132,6 +133,7 @@
|
|
|
132
133
|
"test:provider-fallback": "pnpm exec tsx test/continuous-test-suite-provider-fallback.ts",
|
|
133
134
|
"test:provider-fallback-latency": "pnpm exec tsx test/continuous-test-suite-provider-fallback-latency.ts",
|
|
134
135
|
"test:provider-wiring": "pnpm exec tsx test/continuous-test-suite-provider-wiring.ts",
|
|
136
|
+
"test:pdf-image-streaming": "pnpm exec tsx test/continuous-test-suite-pdf-image-streaming.ts",
|
|
135
137
|
"test:rag": "pnpm exec tsx test/continuous-test-suite-rag.ts",
|
|
136
138
|
"test:skills": "pnpm exec tsx test/continuous-test-suite-skills.ts",
|
|
137
139
|
"test:servers": "pnpm exec tsx test/continuous-test-suite-servers.ts",
|
|
@@ -251,7 +253,9 @@
|
|
|
251
253
|
"test:anthropic-execution-control": "tsx test/continuous-test-suite-anthropic-execution-control.ts",
|
|
252
254
|
"test:aistudio-loop-characterization": "tsx test/continuous-test-suite-aistudio-loop-characterization.ts",
|
|
253
255
|
"test:docs-mcp": "pnpm exec tsx test/continuous-test-suite-docs-mcp.ts",
|
|
254
|
-
"test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts"
|
|
256
|
+
"test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts",
|
|
257
|
+
"test:toolfree-guard-overhead": "pnpm exec tsx test/continuous-test-suite-toolfree-guard-overhead.ts",
|
|
258
|
+
"test:native-failure-events": "pnpm exec tsx test/continuous-test-suite-native-failure-events.ts"
|
|
255
259
|
},
|
|
256
260
|
"files": [
|
|
257
261
|
"dist",
|