@juspay/neurolink 9.94.5 → 9.94.6

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.
@@ -5,12 +5,12 @@ import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerIma
5
5
  import { CONVERSATION_INSTRUCTIONS, STRUCTURED_OUTPUT_INSTRUCTIONS, } from "../config/conversationMemory.js";
6
6
  import { getAvailableInputTokens } from "../constants/contextWindows.js";
7
7
  import { enforceAggregateFileBudget, FILE_READ_BUDGET_PERCENT, } from "../context/fileTokenBudget.js";
8
- import { SIZE_TIER_THRESHOLDS } from "../types/index.js";
8
+ import { isCSVContent, SIZE_TIER_THRESHOLDS } from "../types/index.js";
9
9
  import { tracers, ATTR, withSpan } from "../telemetry/index.js";
10
- import { NeuroLinkError } from "./errorHandling.js";
10
+ import { ErrorFactory, NeuroLinkError, withTimeout } from "./errorHandling.js";
11
11
  import { FileDetector } from "./fileDetector.js";
12
12
  import { getImageCache } from "./imageCache.js";
13
- import { ImageProcessor } from "./imageProcessor.js";
13
+ import { ImageProcessor, imageUtils } from "./imageProcessor.js";
14
14
  import { logger } from "./logger.js";
15
15
  import { PDFImageConverter, PDFProcessor } from "./pdfProcessor.js";
16
16
  import { urlDownloadRateLimiter } from "./rateLimiter.js";
@@ -860,7 +860,11 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
860
860
  }
861
861
  catch (error) {
862
862
  const errMsg = error instanceof Error ? error.message : String(error);
863
- logger.error(`[NEUROLINK] File skipped/failed: ${filename}reason: ${errMsg}`);
863
+ // #273: don't silently drop a failed filelog, then throw so the
864
+ // caller learns the file couldn't be processed (matches the explicit
865
+ // pdf/csv paths' fail-loud behavior).
866
+ logger.error(`[NEUROLINK] File processing failed: ${filename} — reason: ${errMsg}`);
867
+ throw ErrorFactory.fileProcessingFailed(filename, error instanceof Error ? error : new Error(errMsg));
864
868
  }
865
869
  }
866
870
  span.setAttribute(ATTR.FILE_INCLUDED_COUNT, includedCount);
@@ -935,10 +939,12 @@ async function processExplicitCsvFiles(options) {
935
939
  logger.info(`[CSV] ✅ Processed: ${filename}`);
936
940
  }
937
941
  catch (error) {
938
- logger.error(`[CSV] ❌ Failed:`, error);
939
942
  const filename = extractFilename(csvFile, i);
940
- options.input.text += `\n\n## CSV Data Error: Failed to process "${filename}"`;
941
- options.input.text += `\nReason: ${error instanceof Error ? error.message : "Unknown error"}`;
943
+ const errMsg = error instanceof Error ? error.message : String(error);
944
+ // #273: fail loud instead of embedding the error into the prompt text
945
+ // (which the model would then "analyze"). Log, then throw.
946
+ logger.error(`[CSV] ❌ Failed to process ${filename}: ${errMsg}`);
947
+ throw ErrorFactory.csvProcessingFailed(filename, error instanceof Error ? error : new Error(errMsg));
942
948
  }
943
949
  }
944
950
  }
@@ -1067,6 +1073,18 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1067
1073
  // local const so TypeScript sees the definite (non-optional) type in the
1068
1074
  // rest of this function, avoiding 60+ "possibly undefined" errors.
1069
1075
  const inp = options.input;
1076
+ // #284: audioFiles/videoFiles have no dedicated processor — route them
1077
+ // through the same auto-detecting `files` pipeline that already
1078
+ // understands "audio"/"video" FileDetector results (see
1079
+ // appendDetectedFileResult), instead of silently dropping them once
1080
+ // detectMultimodal() routes an audio/video-only request here.
1081
+ if (inp.audioFiles?.length || inp.videoFiles?.length) {
1082
+ inp.files = [
1083
+ ...(inp.files || []),
1084
+ ...(inp.audioFiles || []),
1085
+ ...(inp.videoFiles || []),
1086
+ ];
1087
+ }
1070
1088
  // Compute provider-specific max PDF size once for consistent validation
1071
1089
  const pdfConfig = PDFProcessor.getProviderConfig(provider);
1072
1090
  const maxSize = pdfConfig
@@ -1088,6 +1106,13 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1088
1106
  const hasPDFs = pdfFiles.length > 0;
1089
1107
  // If no images or PDFs, use standard message building and convert to MultimodalChatMessage[]
1090
1108
  if (!hasImages && !hasPDFs) {
1109
+ // #289: CSV content[] items don't need vision, so they never reach the
1110
+ // multimodal converter below — process them into the prompt text here
1111
+ // (otherwise a `content: [{type:"csv"}]`-only request silently drops it).
1112
+ const csvContentItems = inp.content?.filter(isCSVContent) ?? [];
1113
+ if (csvContentItems.length > 0) {
1114
+ inp.text = await appendCsvContentToText(csvContentItems, inp.text ?? "");
1115
+ }
1091
1116
  if (inp.csvFiles) {
1092
1117
  inp.csvFiles = [];
1093
1118
  }
@@ -1216,6 +1241,47 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1216
1241
  throw error;
1217
1242
  }
1218
1243
  }
1244
+ /**
1245
+ * Timeout for detecting/parsing an in-memory CSV `content[]` buffer (#325
1246
+ * review, round 2). Bounds a stalled detector rather than blocking the
1247
+ * request indefinitely.
1248
+ */
1249
+ const CSV_CONTENT_DETECTION_TIMEOUT_MS = 30_000;
1250
+ /**
1251
+ * #289: process CSV `content[]` items into appended prompt text. CSV is
1252
+ * delivered to the model as text (like the explicit `csvFiles` path), so this
1253
+ * is shared by both the no-vision gate and the multimodal converter.
1254
+ */
1255
+ async function appendCsvContentToText(csvItems, baseText) {
1256
+ let text = baseText;
1257
+ for (const csv of csvItems) {
1258
+ const raw = csv.data;
1259
+ if (raw === undefined) {
1260
+ continue;
1261
+ }
1262
+ // #325: raw CSV text (e.g. "a,b\n1,2") is not base64 — decoding it as
1263
+ // base64 silently corrupts the content. Only treat the string as base64
1264
+ // when it actually validates as such; otherwise treat it as literal
1265
+ // UTF-8 CSV text, matching how Buffer inputs are already handled as-is.
1266
+ const buffer = typeof raw === "string"
1267
+ ? imageUtils.isValidBase64(raw)
1268
+ ? Buffer.from(raw, "base64")
1269
+ : Buffer.from(raw, "utf-8")
1270
+ : raw;
1271
+ const name = csv.metadata?.filename || "data.csv";
1272
+ // #325 review (round 2): a stalled/hung detector must not block the
1273
+ // request indefinitely — wrap with the project's standard withTimeout.
1274
+ const result = await withTimeout(FileDetector.detectAndProcess(buffer, {
1275
+ allowedTypes: ["csv"],
1276
+ csvOptions: {
1277
+ maxRows: csv.metadata?.maxRows,
1278
+ formatStyle: csv.metadata?.formatStyle,
1279
+ },
1280
+ }), CSV_CONTENT_DETECTION_TIMEOUT_MS, new Error(`Timed out processing CSV content "${name}" after ${CSV_CONTENT_DETECTION_TIMEOUT_MS}ms`));
1281
+ text += `${text ? "\n\n" : ""}## CSV Data from ${name}\n${result.content}`;
1282
+ }
1283
+ return text;
1284
+ }
1219
1285
  /**
1220
1286
  * Convert advanced content format to provider-specific format
1221
1287
  */
@@ -1223,14 +1289,19 @@ async function convertContentToProviderFormat(content, provider, _model) {
1223
1289
  const textContent = content.find((c) => c.type === "text");
1224
1290
  const imageContent = content.filter((c) => c.type === "image");
1225
1291
  const pdfContent = content.filter((c) => c.type === "pdf");
1292
+ const csvContent = content.filter(isCSVContent);
1226
1293
  // Allow empty text when multimodal content is present (enables image-only or PDF-only queries)
1227
- const text = textContent?.text || "";
1294
+ let text = textContent?.text || "";
1295
+ // #289: CSV content[] items were silently dropped — fold each into the text.
1296
+ if (csvContent.length > 0) {
1297
+ text = await appendCsvContentToText(csvContent, text);
1298
+ }
1228
1299
  const hasMultimodal = imageContent.length > 0 || pdfContent.length > 0;
1229
1300
  // Validate that we have at least some content
1230
1301
  if (!hasMultimodal && !text) {
1231
1302
  throw new Error("Content must include either text or multimodal content");
1232
1303
  }
1233
- // Text-only case
1304
+ // Text-only case (CSV has already been folded into `text`)
1234
1305
  if (imageContent.length === 0 && pdfContent.length === 0) {
1235
1306
  return text;
1236
1307
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.94.5",
3
+ "version": "9.94.6",
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": {