@juspay/neurolink 12.0.2 → 12.0.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 +331 -326
- package/dist/neurolink.d.ts +18 -0
- package/dist/neurolink.js +71 -3
- package/dist/rag/ragIntegration.js +42 -4
- package/dist/utils/messageBuilder.js +130 -60
- package/dist/utils/transformationUtils.d.ts +5 -0
- package/dist/utils/transformationUtils.js +25 -0
- package/package.json +1 -1
package/dist/neurolink.d.ts
CHANGED
|
@@ -85,6 +85,23 @@ export declare class NeuroLink {
|
|
|
85
85
|
/** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
|
|
86
86
|
private mcpArtifactStore?;
|
|
87
87
|
private _disableToolCacheForCurrentRequest;
|
|
88
|
+
/**
|
|
89
|
+
* (toolName + args) keys already served during the CURRENT request.
|
|
90
|
+
* A repeat occurrence within one request bypasses the tool-result cache:
|
|
91
|
+
* when the model deliberately re-calls a tool with identical args in the
|
|
92
|
+
* same turn (a counter, a poll, "check status again"), it wants fresh
|
|
93
|
+
* state — serving the memoized first result silently freezes stateful
|
|
94
|
+
* tools (observed live: a 5-round counter loop executed once). Cross-
|
|
95
|
+
* request dedup — BZ-664's actual goal — is untouched: the first
|
|
96
|
+
* occurrence in a request may still be served from cache. Request-scoped
|
|
97
|
+
* like _disableToolCacheForCurrentRequest above (assigned a fresh Set at
|
|
98
|
+
* request start so the router's save/restore-by-reference pattern works).
|
|
99
|
+
*/
|
|
100
|
+
private _toolCacheKeysServedThisRequest;
|
|
101
|
+
/** True only while a generate()/stream() turn is executing — the
|
|
102
|
+
* repeat-call cache bypass applies inside a turn; direct executeTool
|
|
103
|
+
* calls keep full BZ-664 cache semantics. */
|
|
104
|
+
private _generationTurnActive;
|
|
88
105
|
private mcpEnhancementsConfig?;
|
|
89
106
|
private toolCircuitBreakers;
|
|
90
107
|
private toolExecutionMetrics;
|
|
@@ -1524,6 +1541,7 @@ export declare class NeuroLink {
|
|
|
1524
1541
|
* - Annotations: skip cache for destructive tools, retry safe tools on failure
|
|
1525
1542
|
* - Middleware: apply global middleware chain before execution
|
|
1526
1543
|
*/
|
|
1544
|
+
private toolCacheRepeatKey;
|
|
1527
1545
|
private executeToolInternal;
|
|
1528
1546
|
/**
|
|
1529
1547
|
* Get tool annotations for execution decisions (cache, retry).
|
package/dist/neurolink.js
CHANGED
|
@@ -377,6 +377,23 @@ export class NeuroLink {
|
|
|
377
377
|
/** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
|
|
378
378
|
mcpArtifactStore;
|
|
379
379
|
_disableToolCacheForCurrentRequest = false;
|
|
380
|
+
/**
|
|
381
|
+
* (toolName + args) keys already served during the CURRENT request.
|
|
382
|
+
* A repeat occurrence within one request bypasses the tool-result cache:
|
|
383
|
+
* when the model deliberately re-calls a tool with identical args in the
|
|
384
|
+
* same turn (a counter, a poll, "check status again"), it wants fresh
|
|
385
|
+
* state — serving the memoized first result silently freezes stateful
|
|
386
|
+
* tools (observed live: a 5-round counter loop executed once). Cross-
|
|
387
|
+
* request dedup — BZ-664's actual goal — is untouched: the first
|
|
388
|
+
* occurrence in a request may still be served from cache. Request-scoped
|
|
389
|
+
* like _disableToolCacheForCurrentRequest above (assigned a fresh Set at
|
|
390
|
+
* request start so the router's save/restore-by-reference pattern works).
|
|
391
|
+
*/
|
|
392
|
+
_toolCacheKeysServedThisRequest = new Set();
|
|
393
|
+
/** True only while a generate()/stream() turn is executing — the
|
|
394
|
+
* repeat-call cache bypass applies inside a turn; direct executeTool
|
|
395
|
+
* calls keep full BZ-664 cache semantics. */
|
|
396
|
+
_generationTurnActive = false;
|
|
380
397
|
mcpEnhancementsConfig;
|
|
381
398
|
// Enhanced error handling support
|
|
382
399
|
toolCircuitBreakers = new Map();
|
|
@@ -3509,6 +3526,8 @@ Current user's request: ${currentInput}`;
|
|
|
3509
3526
|
}
|
|
3510
3527
|
finally {
|
|
3511
3528
|
this._disableToolCacheForCurrentRequest = false;
|
|
3529
|
+
this._toolCacheKeysServedThisRequest = new Set();
|
|
3530
|
+
this._generationTurnActive = false;
|
|
3512
3531
|
generateSpan.end();
|
|
3513
3532
|
}
|
|
3514
3533
|
}
|
|
@@ -3527,6 +3546,8 @@ Current user's request: ${currentInput}`;
|
|
|
3527
3546
|
await this.resolveDynamicOptions(options);
|
|
3528
3547
|
options.model = resolveModel(options.model, this.modelAliasConfig);
|
|
3529
3548
|
this._disableToolCacheForCurrentRequest = !!options.disableToolCache;
|
|
3549
|
+
this._toolCacheKeysServedThisRequest = new Set();
|
|
3550
|
+
this._generationTurnActive = true;
|
|
3530
3551
|
generateSpan.setAttribute("neurolink.provider", options.provider || "default");
|
|
3531
3552
|
generateSpan.setAttribute("neurolink.model", options.model || "default");
|
|
3532
3553
|
generateSpan.setAttribute("neurolink.input_length", typeof optionsOrPrompt === "string"
|
|
@@ -5527,7 +5548,15 @@ Current user's request: ${currentInput}`;
|
|
|
5527
5548
|
: "empty",
|
|
5528
5549
|
hasCustomSystemPrompt: !!options.systemPrompt,
|
|
5529
5550
|
});
|
|
5530
|
-
|
|
5551
|
+
// Caller-supplied conversationMessages win — mirroring
|
|
5552
|
+
// directProviderGeneration. getConversationMessages() returns [] whenever
|
|
5553
|
+
// no memory manager / session context is configured, which silently
|
|
5554
|
+
// dropped an inline history on the MCP-first path (the default path for
|
|
5555
|
+
// any bare `new NeuroLink()` with tools enabled) while the direct path
|
|
5556
|
+
// honored it.
|
|
5557
|
+
const conversationMessages = (options.conversationMessages?.length
|
|
5558
|
+
? options.conversationMessages
|
|
5559
|
+
: await getConversationMessages(this.conversationMemory, options));
|
|
5531
5560
|
this.logMCPConversationSummary(requestId, conversationMessages);
|
|
5532
5561
|
logger.debug("[Observability] Available tools for LLM", {
|
|
5533
5562
|
requestId,
|
|
@@ -6672,6 +6701,8 @@ Current user's request: ${currentInput}`;
|
|
|
6672
6701
|
const streamIsRoot = !trace.getSpan(context.active());
|
|
6673
6702
|
const spanStartTime = Date.now();
|
|
6674
6703
|
this._disableToolCacheForCurrentRequest = !!options.disableToolCache;
|
|
6704
|
+
this._toolCacheKeysServedThisRequest = new Set();
|
|
6705
|
+
this._generationTurnActive = true;
|
|
6675
6706
|
try {
|
|
6676
6707
|
options.model = resolveModel(options.model, this.modelAliasConfig);
|
|
6677
6708
|
const startTime = Date.now();
|
|
@@ -7304,6 +7335,8 @@ Current user's request: ${currentInput}`;
|
|
|
7304
7335
|
}
|
|
7305
7336
|
finally {
|
|
7306
7337
|
self._disableToolCacheForCurrentRequest = false;
|
|
7338
|
+
self._toolCacheKeysServedThisRequest = new Set();
|
|
7339
|
+
self._generationTurnActive = false;
|
|
7307
7340
|
params.streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - params.spanStartTime);
|
|
7308
7341
|
params.streamSpan.end();
|
|
7309
7342
|
}
|
|
@@ -7603,6 +7636,8 @@ Current user's request: ${currentInput}`;
|
|
|
7603
7636
|
}
|
|
7604
7637
|
}
|
|
7605
7638
|
self._disableToolCacheForCurrentRequest = false;
|
|
7639
|
+
self._toolCacheKeysServedThisRequest = new Set();
|
|
7640
|
+
self._generationTurnActive = false;
|
|
7606
7641
|
cleanupListeners();
|
|
7607
7642
|
streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - spanStartTime);
|
|
7608
7643
|
streamSpan.setAttribute(ATTR.NL_OUTPUT_LENGTH, accumulatedContent.length);
|
|
@@ -10047,6 +10082,16 @@ Current user's request: ${currentInput}`;
|
|
|
10047
10082
|
* - Annotations: skip cache for destructive tools, retry safe tools on failure
|
|
10048
10083
|
* - Middleware: apply global middleware chain before execution
|
|
10049
10084
|
*/
|
|
10085
|
+
toolCacheRepeatKey(toolName, params) {
|
|
10086
|
+
try {
|
|
10087
|
+
return `${toolName}:${JSON.stringify(params) ?? ""}`;
|
|
10088
|
+
}
|
|
10089
|
+
catch {
|
|
10090
|
+
// Unserializable args (circular refs) — no repeat tracking; the
|
|
10091
|
+
// ToolResultCache's own keying handles (or rejects) them as before.
|
|
10092
|
+
return undefined;
|
|
10093
|
+
}
|
|
10094
|
+
}
|
|
10050
10095
|
async executeToolInternal(toolName, params, options, HITLState) {
|
|
10051
10096
|
const functionTag = "NeuroLink.executeToolInternal";
|
|
10052
10097
|
// === MCP ENHANCEMENT: Infer annotations for cache/retry decisions ===
|
|
@@ -10064,13 +10109,24 @@ Current user's request: ${currentInput}`;
|
|
|
10064
10109
|
__ctx: options.authContext ?? this.toolExecutionContext,
|
|
10065
10110
|
}
|
|
10066
10111
|
: params;
|
|
10067
|
-
|
|
10112
|
+
const repeatKey = this._generationTurnActive
|
|
10113
|
+
? this.toolCacheRepeatKey(toolName, cacheParams)
|
|
10114
|
+
: undefined;
|
|
10115
|
+
const isRepeatCallThisRequest = repeatKey !== undefined &&
|
|
10116
|
+
this._toolCacheKeysServedThisRequest.has(repeatKey);
|
|
10117
|
+
if (repeatKey !== undefined) {
|
|
10118
|
+
this._toolCacheKeysServedThisRequest.add(repeatKey);
|
|
10119
|
+
}
|
|
10120
|
+
if (isCacheEnabled && toolResultCache && !isRepeatCallThisRequest) {
|
|
10068
10121
|
const cached = toolResultCache.getCachedResult(toolName, cacheParams);
|
|
10069
10122
|
if (cached !== undefined) {
|
|
10070
10123
|
logger.debug(`[${functionTag}] Cache HIT for tool: ${toolName}`);
|
|
10071
10124
|
return cached;
|
|
10072
10125
|
}
|
|
10073
10126
|
}
|
|
10127
|
+
else if (isRepeatCallThisRequest) {
|
|
10128
|
+
logger.debug(`[${functionTag}] Repeat call within this request — bypassing tool cache for: ${toolName}`);
|
|
10129
|
+
}
|
|
10074
10130
|
// === MCP ENHANCEMENT: Middleware chain wrapper ===
|
|
10075
10131
|
const executeWithMiddleware = async (executeFn) => {
|
|
10076
10132
|
if (this.mcpToolMiddlewares.length === 0) {
|
|
@@ -11515,7 +11571,19 @@ Current user's request: ${currentInput}`;
|
|
|
11515
11571
|
? { __ctx: this.toolExecutionContext }
|
|
11516
11572
|
: {}),
|
|
11517
11573
|
};
|
|
11518
|
-
|
|
11574
|
+
// Same repeat-within-request bypass as executeToolInternal: a model
|
|
11575
|
+
// re-calling the identical tool+args in one turn wants fresh state.
|
|
11576
|
+
const externalRepeatKey = this._generationTurnActive
|
|
11577
|
+
? this.toolCacheRepeatKey(toolName, cacheKeyArgs)
|
|
11578
|
+
: undefined;
|
|
11579
|
+
const isExternalRepeatThisRequest = externalRepeatKey !== undefined &&
|
|
11580
|
+
this._toolCacheKeysServedThisRequest.has(externalRepeatKey);
|
|
11581
|
+
if (externalRepeatKey !== undefined) {
|
|
11582
|
+
this._toolCacheKeysServedThisRequest.add(externalRepeatKey);
|
|
11583
|
+
}
|
|
11584
|
+
if (cacheEnabled &&
|
|
11585
|
+
this.mcpToolResultCache &&
|
|
11586
|
+
!isExternalRepeatThisRequest) {
|
|
11519
11587
|
const cached = this.mcpToolResultCache.getCachedResult(toolName, cacheKeyArgs);
|
|
11520
11588
|
if (cached !== undefined) {
|
|
11521
11589
|
mcpLogger.debug(`[NeuroLink] Tool result cache HIT: ${toolName} on ${serverId}`);
|
|
@@ -235,9 +235,47 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
|
|
|
235
235
|
const EMBEDDING_DIMENSION = 128;
|
|
236
236
|
const vectorStore = new InMemoryVectorStore();
|
|
237
237
|
const indexName = "rag-index";
|
|
238
|
+
// When the caller configured an embedding provider/model, embed BOTH the
|
|
239
|
+
// index chunks and (below) the queries through that provider — previously
|
|
240
|
+
// those config fields had no runtime effect and retrieval always used the
|
|
241
|
+
// deterministic hash embedding, which is a lexical fingerprint rather than
|
|
242
|
+
// a semantic space. Index and query must share one embedding space, so the
|
|
243
|
+
// provider path replaces the hash path wholesale; any provider failure
|
|
244
|
+
// falls back to the hash for both sides.
|
|
245
|
+
const wantProviderEmbeddings = Boolean(embeddingProvider || embeddingModel);
|
|
246
|
+
const embedProviderName = embeddingProvider || fallbackProvider || "vertex";
|
|
247
|
+
const embedModelName = embeddingModel || "gemini-2.5-flash";
|
|
248
|
+
let embedFn = (text) => Promise.resolve(generateSimpleEmbedding(text, EMBEDDING_DIMENSION));
|
|
249
|
+
if (wantProviderEmbeddings) {
|
|
250
|
+
try {
|
|
251
|
+
const { AIProviderFactory } = await import("../core/factory.js");
|
|
252
|
+
const embedderProvider = (await AIProviderFactory.createProvider(embedProviderName, embedModelName));
|
|
253
|
+
if (typeof embedderProvider.embed === "function") {
|
|
254
|
+
const providerEmbed = embedderProvider.embed.bind(embedderProvider);
|
|
255
|
+
embedFn = (text) => providerEmbed(text, embedModelName);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
logger.warn(`[RAG] Embedding provider '${embedProviderName}' has no embed(); falling back to hash embeddings`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
logger.warn("[RAG] Failed to create embedding provider; falling back to hash embeddings", { error: error instanceof Error ? error.message : String(error) });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
let chunkVectors;
|
|
266
|
+
try {
|
|
267
|
+
chunkVectors = await Promise.all(allChunks.map((chunk) => embedFn(chunk.text)));
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
// One failed chunk must not leave a mixed-space index — flip the whole
|
|
271
|
+
// index AND all queries back to the hash space together.
|
|
272
|
+
logger.warn("[RAG] Provider embedding failed mid-index; falling back to hash embeddings for index and queries", { error: error instanceof Error ? error.message : String(error) });
|
|
273
|
+
embedFn = (text) => Promise.resolve(generateSimpleEmbedding(text, EMBEDDING_DIMENSION));
|
|
274
|
+
chunkVectors = allChunks.map((chunk) => generateSimpleEmbedding(chunk.text, EMBEDDING_DIMENSION));
|
|
275
|
+
}
|
|
238
276
|
const items = allChunks.map((chunk, i) => ({
|
|
239
277
|
id: `rag-chunk-${i}`,
|
|
240
|
-
vector:
|
|
278
|
+
vector: chunkVectors[i],
|
|
241
279
|
metadata: {
|
|
242
280
|
text: chunk.text,
|
|
243
281
|
...chunk.metadata,
|
|
@@ -274,9 +312,9 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
|
|
|
274
312
|
"rag.top_k": topK ?? 5,
|
|
275
313
|
},
|
|
276
314
|
}, async (span) => {
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
const queryEmbedding = generateSimpleEmbedding(query, EMBEDDING_DIMENSION);
|
|
315
|
+
// Query through the same embedding space the index was built in —
|
|
316
|
+
// provider embeddings when configured (and healthy), else the hash.
|
|
317
|
+
const queryEmbedding = await embedFn(query).catch(() => generateSimpleEmbedding(query, EMBEDDING_DIMENSION));
|
|
280
318
|
// Fetch more candidates than needed so diversity can select across files
|
|
281
319
|
const fetchK = fileContents.length > 1 ? topK * 3 : topK;
|
|
282
320
|
const rawResults = await vectorStore.query({
|
|
@@ -2184,76 +2184,146 @@ pdfFiles, provider, model, audioFiles = []) {
|
|
|
2184
2184
|
}));
|
|
2185
2185
|
}
|
|
2186
2186
|
else {
|
|
2187
|
-
//
|
|
2188
|
-
//
|
|
2189
|
-
|
|
2187
|
+
// No native PDF support: inline the PDF's text layer, clearly labeled per
|
|
2188
|
+
// file. Image-only conversion sent content a text-only backend can never
|
|
2189
|
+
// read — observed live as the model flatly claiming no file was attached
|
|
2190
|
+
// — and for proxy providers (litellm/openrouter) supportsVision() is a
|
|
2191
|
+
// pass-through, so vision cannot be trusted to carry the content either.
|
|
2192
|
+
// Text is therefore always included (primary for text-only backends,
|
|
2193
|
+
// extra grounding otherwise); page images are appended below only when
|
|
2194
|
+
// the provider may actually see them. A PDF with no text layer (pure
|
|
2195
|
+
// scan) gets an explicit note rather than silence, so the model
|
|
2196
|
+
// acknowledges an unreadable attachment instead of denying it exists.
|
|
2197
|
+
const providerCanSeeImages = ProviderImageAdapter.supportsVision(provider, model);
|
|
2190
2198
|
for (const pdf of pdfFiles) {
|
|
2199
|
+
const name = safeBasename(pdf.filename);
|
|
2191
2200
|
try {
|
|
2192
|
-
const
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
// and it used to hardcode scale 2.0 — silently overriding the
|
|
2196
|
-
// lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
|
|
2197
|
-
// issue reports (a 100-page render at 2.0 is ~776MB; 1.5 is ~44%
|
|
2198
|
-
// fewer pixels per page). Callers can raise it back per request.
|
|
2199
|
-
scale: pdf.scale ?? PDF_LIMITS.DEFAULT_SCALE,
|
|
2200
|
-
// Page ceiling guards token overflow; also now caller-adjustable
|
|
2201
|
-
// rather than a constant nothing could reach.
|
|
2202
|
-
maxPages: effectiveMaxPages,
|
|
2201
|
+
const { PDFParse } = await import("pdf-parse");
|
|
2202
|
+
const parser = new PDFParse({
|
|
2203
|
+
data: new Uint8Array(pdf.buffer),
|
|
2203
2204
|
...(pdf.password ? { password: pdf.password } : {}), // #258
|
|
2204
|
-
...(pdf.maxCanvasPixels
|
|
2205
|
-
? { maxCanvasPixels: pdf.maxCanvasPixels }
|
|
2206
|
-
: {}), // #260
|
|
2207
2205
|
});
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
`failed to render and were omitted (page ${conversionResult.errors.map((e) => e.page).join(", ")}).`);
|
|
2206
|
+
try {
|
|
2207
|
+
const extracted = await parser.getText();
|
|
2208
|
+
const pdfText = (extracted?.text ?? "").trim();
|
|
2209
|
+
if (pdfText.length > 0) {
|
|
2210
|
+
content.push({
|
|
2211
|
+
type: "text",
|
|
2212
|
+
text: `\n[Attached PDF: ${name}]\n${pdfText}\n[End of PDF: ${name}]`,
|
|
2213
|
+
});
|
|
2214
|
+
logger.info(`[PDF→Text] ✅ Extracted text for non-vision provider ${provider}: ${name} (${pdfText.length} chars)`);
|
|
2215
|
+
}
|
|
2216
|
+
else {
|
|
2217
|
+
content.push({
|
|
2218
|
+
type: "text",
|
|
2219
|
+
text: `\n[Attached PDF: ${name} — no extractable text layer (likely a scanned document); its contents are unavailable to this text-only model.]`,
|
|
2220
|
+
});
|
|
2221
|
+
logger.warn(`[PDF→Text] ${name} has no text layer; provider ${provider} cannot see images — content unavailable`);
|
|
2222
|
+
}
|
|
2226
2223
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
conversionResult.images.forEach((base64Image, pageIndex) => {
|
|
2230
|
-
content.push({
|
|
2231
|
-
type: "image",
|
|
2232
|
-
image: base64Image,
|
|
2233
|
-
mimeType: "image/png",
|
|
2234
|
-
});
|
|
2235
|
-
logger.debug(`[PDF→Image] Added page ${pageIndex + 1}/${conversionResult.pageCount} of ${pdf.filename}`);
|
|
2236
|
-
});
|
|
2237
|
-
// Log any warnings from conversion
|
|
2238
|
-
if (conversionResult.warnings) {
|
|
2239
|
-
conversionResult.warnings.forEach((warning) => {
|
|
2240
|
-
logger.warn(`[PDF→Image] ${warning}`);
|
|
2241
|
-
});
|
|
2224
|
+
finally {
|
|
2225
|
+
await parser.destroy?.();
|
|
2242
2226
|
}
|
|
2243
2227
|
}
|
|
2244
2228
|
catch (error) {
|
|
2245
2229
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2246
|
-
logger.error(`[PDF→
|
|
2247
|
-
// #258: password errors are
|
|
2248
|
-
//
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2230
|
+
logger.error(`[PDF→Text] ❌ Failed to parse ${name}: ${errorMessage}`);
|
|
2231
|
+
// #258: password errors are actionable and must surface. pdf-parse
|
|
2232
|
+
// raises pdf.js's PasswordException (not our typed PDF_PASSWORD_*
|
|
2233
|
+
// codes), so detect by name/message shape. When the provider can see
|
|
2234
|
+
// images, defer the throw to the image-conversion path below — it
|
|
2235
|
+
// raises the canonical typed error with the "supply the password"
|
|
2236
|
+
// guidance callers and tests rely on.
|
|
2237
|
+
const isPasswordError = error?.name === "PasswordException" ||
|
|
2238
|
+
/password/i.test(errorMessage);
|
|
2239
|
+
if (isPasswordError) {
|
|
2240
|
+
if (!providerCanSeeImages) {
|
|
2241
|
+
throw error;
|
|
2242
|
+
}
|
|
2243
|
+
// Image branch will produce the canonical typed password error.
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
content.push({
|
|
2247
|
+
type: "text",
|
|
2248
|
+
text: `\n[Attached PDF: ${name} — could not be parsed (${errorMessage}); its contents are unavailable.]`,
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
// Page images in addition to the text, for providers that may actually
|
|
2253
|
+
// see them (vision models, and proxies whose upstream might).
|
|
2254
|
+
if (providerCanSeeImages) {
|
|
2255
|
+
logger.info(`[PDF→Image] Provider ${provider} doesn't support native PDF. Converting ${pdfFiles.length} PDF(s) to images...`);
|
|
2256
|
+
for (const pdf of pdfFiles) {
|
|
2257
|
+
try {
|
|
2258
|
+
const effectiveMaxPages = pdf.maxPages ?? PDF_LIMITS.DEFAULT_MAX_PAGES;
|
|
2259
|
+
const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
|
|
2260
|
+
// #297: this is the only PDF→image call the product actually makes,
|
|
2261
|
+
// and it used to hardcode scale 2.0 — silently overriding the
|
|
2262
|
+
// lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
|
|
2263
|
+
// issue reports (a 100-page render at 2.0 is ~776MB; 1.5 is ~44%
|
|
2264
|
+
// fewer pixels per page). Callers can raise it back per request.
|
|
2265
|
+
scale: pdf.scale ?? PDF_LIMITS.DEFAULT_SCALE,
|
|
2266
|
+
// Page ceiling guards token overflow; also now caller-adjustable
|
|
2267
|
+
// rather than a constant nothing could reach.
|
|
2268
|
+
maxPages: effectiveMaxPages,
|
|
2269
|
+
...(pdf.password ? { password: pdf.password } : {}), // #258
|
|
2270
|
+
...(pdf.maxCanvasPixels
|
|
2271
|
+
? { maxCanvasPixels: pdf.maxCanvasPixels }
|
|
2272
|
+
: {}), // #260
|
|
2273
|
+
});
|
|
2274
|
+
// The renderer stops at maxPages, so a longer document is silently
|
|
2275
|
+
// truncated — say so rather than letting the model answer from a
|
|
2276
|
+
// partial document as though it had the whole thing.
|
|
2277
|
+
//
|
|
2278
|
+
// Keyed on the cap being reached, not on pdf.pageCount: that field is
|
|
2279
|
+
// null whenever `input.content` omits `metadata.pages`, which is the
|
|
2280
|
+
// common case, so a page-count comparison would simply never fire
|
|
2281
|
+
// there. Reaching the cap is also unambiguous — a short count caused by
|
|
2282
|
+
// per-page render failures (#294 isolates those into `errors`) would
|
|
2283
|
+
// otherwise be misreported as a maxPages truncation.
|
|
2284
|
+
if (conversionResult.pageCount >= effectiveMaxPages) {
|
|
2285
|
+
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)} hit the ${effectiveMaxPages}-page ` +
|
|
2286
|
+
`conversion limit. Any pages beyond that were not sent — the model may be ` +
|
|
2287
|
+
`answering from a partial document. Raise pdfOptions.maxPages or split the file.`);
|
|
2288
|
+
}
|
|
2289
|
+
if (conversionResult.errors && conversionResult.errors.length > 0) {
|
|
2290
|
+
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${conversionResult.errors.length} page(s) ` +
|
|
2291
|
+
`failed to render and were omitted (page ${conversionResult.errors.map((e) => e.page).join(", ")}).`);
|
|
2292
|
+
}
|
|
2293
|
+
logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
|
|
2294
|
+
// Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
|
|
2295
|
+
conversionResult.images.forEach((base64Image, pageIndex) => {
|
|
2296
|
+
content.push({
|
|
2297
|
+
type: "image",
|
|
2298
|
+
image: base64Image,
|
|
2299
|
+
mimeType: "image/png",
|
|
2300
|
+
});
|
|
2301
|
+
logger.debug(`[PDF→Image] Added page ${pageIndex + 1}/${conversionResult.pageCount} of ${pdf.filename}`);
|
|
2302
|
+
});
|
|
2303
|
+
// Log any warnings from conversion
|
|
2304
|
+
if (conversionResult.warnings) {
|
|
2305
|
+
conversionResult.warnings.forEach((warning) => {
|
|
2306
|
+
logger.warn(`[PDF→Image] ${warning}`);
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
catch (error) {
|
|
2311
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2312
|
+
logger.error(`[PDF→Image] ❌ Failed to convert ${pdf.filename}: ${errorMessage}`);
|
|
2313
|
+
// #258: password errors are already actionable typed errors — re-throw
|
|
2314
|
+
// them unwrapped so the "supply a password" guidance isn't buried.
|
|
2315
|
+
const code = error?.code;
|
|
2316
|
+
if (code === "PDF_PASSWORD_REQUIRED" ||
|
|
2317
|
+
code === "PDF_INCORRECT_PASSWORD") {
|
|
2318
|
+
throw error;
|
|
2319
|
+
}
|
|
2320
|
+
// Re-throw so the user knows PDF processing failed. The inlined
|
|
2321
|
+
// text layer above does NOT soften this: conversion failures
|
|
2322
|
+
// include caller mistakes (e.g. an invalid maxCanvasPixels, #260)
|
|
2323
|
+
// that must surface rather than be silently downgraded.
|
|
2324
|
+
throw new Error(`PDF to image conversion failed for ${pdf.filename}: ${errorMessage}. ` +
|
|
2325
|
+
`Provider ${provider} doesn't support native PDFs and image conversion failed.`, { cause: error });
|
|
2253
2326
|
}
|
|
2254
|
-
// Re-throw so the user knows PDF processing failed
|
|
2255
|
-
throw new Error(`PDF to image conversion failed for ${pdf.filename}: ${errorMessage}. ` +
|
|
2256
|
-
`Provider ${provider} doesn't support native PDFs and image conversion failed.`, { cause: error });
|
|
2257
2327
|
}
|
|
2258
2328
|
}
|
|
2259
2329
|
}
|
|
@@ -33,6 +33,11 @@ export declare function transformToolExecutionsForMCP(toolExecutions?: unknown[]
|
|
|
33
33
|
executionTime: number;
|
|
34
34
|
success: boolean;
|
|
35
35
|
serverId?: string;
|
|
36
|
+
params?: unknown;
|
|
37
|
+
output?: unknown;
|
|
38
|
+
startedAt?: number;
|
|
39
|
+
isError?: boolean;
|
|
40
|
+
error?: string;
|
|
36
41
|
}>;
|
|
37
42
|
/**
|
|
38
43
|
* Convert ToolExecutionRecord entries (or legacy-shaped entries) to the
|
|
@@ -234,11 +234,36 @@ export function transformToolExecutionsForMCP(toolExecutions) {
|
|
|
234
234
|
toolCall.server ||
|
|
235
235
|
undefined;
|
|
236
236
|
}
|
|
237
|
+
// Pass the execution payload through instead of discarding it. This
|
|
238
|
+
// transform used to keep only {toolName, executionTime, success,
|
|
239
|
+
// serverId}; toToolExecutionRecords() downstream then found no
|
|
240
|
+
// input/output on the entries and emitted params:{} with
|
|
241
|
+
// resultText:"undefined" for every tool execution on the MCP path —
|
|
242
|
+
// even when the source entry was a full record with real args and a
|
|
243
|
+
// real result. Field names chosen to match what
|
|
244
|
+
// toToolExecutionRecords() reads (input/params, output/result/
|
|
245
|
+
// resultText, startedAt, isError).
|
|
246
|
+
const params = teRecord.params ?? teRecord.input ?? teRecord.args ?? undefined;
|
|
247
|
+
const output = teRecord.output ??
|
|
248
|
+
teRecord.result ??
|
|
249
|
+
teRecord.response ??
|
|
250
|
+
(typeof teRecord.resultText === "string"
|
|
251
|
+
? teRecord.resultText
|
|
252
|
+
: undefined);
|
|
237
253
|
return {
|
|
238
254
|
toolName: toolName,
|
|
239
255
|
executionTime: executionTime,
|
|
240
256
|
success: success,
|
|
241
257
|
serverId: serverId,
|
|
258
|
+
...(params !== undefined ? { params } : {}),
|
|
259
|
+
...(output !== undefined ? { output } : {}),
|
|
260
|
+
...(typeof teRecord.startedAt === "number"
|
|
261
|
+
? { startedAt: teRecord.startedAt }
|
|
262
|
+
: {}),
|
|
263
|
+
...(teRecord.isError !== undefined
|
|
264
|
+
? { isError: teRecord.isError === true }
|
|
265
|
+
: { isError: !success }),
|
|
266
|
+
...(typeof teRecord.error === "string" ? { error: teRecord.error } : {}),
|
|
242
267
|
};
|
|
243
268
|
});
|
|
244
269
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.0.
|
|
3
|
+
"version": "12.0.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": {
|