@juspay/neurolink 12.19.2 → 12.20.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/adapters/imageFormatSupport.d.ts +5 -2
  3. package/dist/adapters/imageFormatSupport.js +47 -24
  4. package/dist/browser/neurolink.min.js +394 -394
  5. package/dist/cli/commands/proxy.js +171 -25
  6. package/dist/cli/commands/telemetry.d.ts +4 -1
  7. package/dist/cli/commands/telemetry.js +113 -0
  8. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  9. package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
  10. package/dist/cli/utils/nativeTelemetry.js +322 -0
  11. package/dist/core/modules/MessageBuilder.js +2 -0
  12. package/dist/neurolink.js +1 -0
  13. package/dist/processors/config/fileExtensions.d.ts +8 -1
  14. package/dist/processors/config/fileExtensions.js +8 -1
  15. package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
  16. package/dist/proxy/bodyCapturePolicy.js +132 -0
  17. package/dist/proxy/otelLogSink.d.ts +11 -1
  18. package/dist/proxy/otelLogSink.js +174 -27
  19. package/dist/proxy/otlpLogTransport.d.ts +4 -2
  20. package/dist/proxy/otlpLogTransport.js +22 -7
  21. package/dist/proxy/requestLogger.js +11 -3
  22. package/dist/proxy/restartControl.js +18 -3
  23. package/dist/proxy/rollingProxyServer.js +48 -1
  24. package/dist/proxy/rollingWorkerProcess.js +87 -10
  25. package/dist/proxy/rollingWorkerProtocol.js +4 -2
  26. package/dist/proxy/rollingWorkerSupervisor.js +47 -10
  27. package/dist/proxy/socketWorkerRuntime.js +90 -4
  28. package/dist/proxy/updateChecker.d.ts +3 -3
  29. package/dist/proxy/updateChecker.js +16 -9
  30. package/dist/proxy/updateCoordinator.d.ts +10 -1
  31. package/dist/proxy/updateCoordinator.js +68 -1
  32. package/dist/proxy/updateState.d.ts +2 -0
  33. package/dist/proxy/updateState.js +15 -0
  34. package/dist/rag/pipeline/RAGPipeline.js +29 -0
  35. package/dist/rag/ragIntegration.js +21 -1
  36. package/dist/types/cli.d.ts +18 -0
  37. package/dist/types/file.d.ts +10 -0
  38. package/dist/types/generate.d.ts +27 -1
  39. package/dist/types/proxy.d.ts +97 -3
  40. package/dist/types/stream.d.ts +13 -1
  41. package/dist/utils/imageProcessor.js +151 -6
  42. package/dist/utils/messageBuilder.d.ts +2 -2
  43. package/dist/utils/messageBuilder.js +23 -13
  44. package/dist/utils/multimodalOptionsBuilder.d.ts +3 -0
  45. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  46. package/docs-site/static/search-index.json +1 -1
  47. package/package.json +2 -1
  48. package/scripts/observability/NATIVE-DURABLE.md +68 -0
  49. package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
  50. package/scripts/observability/proxy-telemetry-check.mjs +135 -50
@@ -65,6 +65,153 @@ const RETRYABLE_ERROR_CODES = new Set([
65
65
  "ETIMEDOUT",
66
66
  "ERR_NETWORK",
67
67
  ]);
68
+ /**
69
+ * Bytes of prologue {@link hasSvgRoot} decodes and scans.
70
+ *
71
+ * A real SVG reaches its root within a few hundred bytes, so 4KB is generous.
72
+ * The cap exists because scanning the whole buffer made every unrecognized
73
+ * binary — and every BMP/TIFF/ICO/AVIF, whose checks run after this one — pay
74
+ * a full UTF-8 decode, and let a crafted unterminated doctype walk every byte
75
+ * (measured: 322ms on 32MB, 69ms to reject a 16MB BMP).
76
+ */
77
+ const SVG_PROLOGUE_SCAN_BYTES = 4096;
78
+ /**
79
+ * Find an SVG root after XML's permitted leading constructs.
80
+ *
81
+ * Deliberately a discriminator, not an XML parser. Raster magic is checked
82
+ * first; this only distinguishes text reaching an SVG root after a UTF-8 BOM,
83
+ * XML whitespace, processing instructions, comments, or a doctype. The doctype
84
+ * scanner tracks quotes, comments/PIs, and the internal subset so a `>` or
85
+ * `]` inside any of them cannot expose a later `<svg` early. The root itself
86
+ * may carry an XML namespace prefix (`<s:svg xmlns:s="...">`), which is
87
+ * accepted alongside the bare `<svg` form.
88
+ *
89
+ * Scanning a bounded window makes "unterminated" ambiguous, and the two cases
90
+ * must resolve OPPOSITE ways:
91
+ *
92
+ * - the WINDOW ran out — the rest of the document is unreadable from here, so
93
+ * fail CLOSED and answer SVG. This function gates a rejection, so answering
94
+ * raster on an unterminated prologue would hand back the exact bypass it
95
+ * exists to close: prepend filler past the window and sail through as
96
+ * raster. A raster file never opens `<!--` or `<!doctype`, so the
97
+ * false-positive cost is a skipped image; the other direction is the
98
+ * vulnerability.
99
+ * - the BUFFER ran out — the whole document was read and is malformed, so it
100
+ * is genuinely not an SVG.
101
+ */
102
+ function hasSvgRoot(input) {
103
+ const windowed = input.length > SVG_PROLOGUE_SCAN_BYTES;
104
+ const text = input
105
+ .subarray(0, Math.min(input.length, SVG_PROLOGUE_SCAN_BYTES))
106
+ .toString("utf8");
107
+ let position = text.charCodeAt(0) === 0xfeff ? 1 : 0;
108
+ const skipWhitespace = () => {
109
+ while (/\s/.test(text[position] ?? "")) {
110
+ position++;
111
+ }
112
+ };
113
+ const skipDoctype = () => {
114
+ let quote;
115
+ let subsetDepth = 0;
116
+ let index = position + 9;
117
+ while (index < text.length) {
118
+ const char = text[index];
119
+ if (quote) {
120
+ if (char === quote) {
121
+ quote = undefined;
122
+ }
123
+ index++;
124
+ continue;
125
+ }
126
+ // Comments and processing instructions are valid markup inside the
127
+ // internal subset (and, leniently, anywhere else in the declaration)
128
+ // and may themselves contain `]` or `>` — e.g. `<!-- ] > -->`. Skip
129
+ // them as opaque spans, the same way the outer scan does, so their
130
+ // contents can never be misread as the subset-close or doctype-end
131
+ // delimiter.
132
+ if (text.startsWith("<!--", index)) {
133
+ const end = text.indexOf("-->", index + 4);
134
+ if (end < 0) {
135
+ return false;
136
+ }
137
+ index = end + 3;
138
+ continue;
139
+ }
140
+ if (text.startsWith("<?", index)) {
141
+ const end = text.indexOf("?>", index + 2);
142
+ if (end < 0) {
143
+ return false;
144
+ }
145
+ index = end + 2;
146
+ continue;
147
+ }
148
+ if (char === '"' || char === "'") {
149
+ quote = char;
150
+ }
151
+ else if (char === "[") {
152
+ subsetDepth++;
153
+ }
154
+ else if (char === "]" && subsetDepth > 0) {
155
+ subsetDepth--;
156
+ }
157
+ else if (char === ">" && subsetDepth === 0) {
158
+ position = index + 1;
159
+ return true;
160
+ }
161
+ index++;
162
+ }
163
+ return false;
164
+ };
165
+ skipWhitespace();
166
+ while (position < text.length) {
167
+ if (text.startsWith("<?", position)) {
168
+ const end = text.indexOf("?>", position + 2);
169
+ if (end < 0) {
170
+ return windowed;
171
+ }
172
+ position = end + 2;
173
+ }
174
+ else if (text.startsWith("<!--", position)) {
175
+ const end = text.indexOf("-->", position + 4);
176
+ if (end < 0) {
177
+ return windowed;
178
+ }
179
+ position = end + 3;
180
+ }
181
+ else if (text.slice(position, position + 9).toLowerCase() === "<!doctype") {
182
+ if (!skipDoctype()) {
183
+ return windowed;
184
+ }
185
+ }
186
+ else {
187
+ break;
188
+ }
189
+ skipWhitespace();
190
+ }
191
+ const rest = text.slice(position);
192
+ // Accepts an optional XML namespace prefix ahead of the tag name — an
193
+ // NCName (letters/digits/`.`/`-`/`_`, not starting with a digit) followed
194
+ // by `:` — so `<s:svg xmlns:s="...">` matches the same way bare `<svg`
195
+ // does.
196
+ if (/^<(?:[A-Za-z_][\w.-]*:)?svg(?:[\s/>])/i.test(rest)) {
197
+ return true;
198
+ }
199
+ // A fixed "`<svg` plus its delimiter is 5 characters" cutoff only worked
200
+ // when the tag name was the literal `svg`. With a namespace prefix now
201
+ // accepted, the prefix is unbounded (any NCName), so no fixed length can
202
+ // bound "may still be mid-token" — `<s:svg` (6 chars) is just as
203
+ // truncatable as `<svg` (4) was. Instead, check whether `rest` could still
204
+ // be the START of a string this function would accept: an optional NCName
205
+ // prefix, optionally followed by `:` and however much of the literal
206
+ // `svg` has been seen so far, with nothing left over. Anything left over
207
+ // (a character that cannot continue the prefix, isn't `:`, or doesn't
208
+ // continue "svg") proves `rest` can never complete into a match, so it is
209
+ // safe to answer false even though the window is cut. An empty `rest` is
210
+ // the same ambiguity in the degenerate case (no root text observed yet).
211
+ const isUnresolvedPrefix = rest.length === 0 ||
212
+ /^<(?:[A-Za-z_][\w.-]*)?(?::(?:s(?:v(?:g)?)?)?)?$/i.test(rest);
213
+ return windowed && isUnresolvedPrefix;
214
+ }
68
215
  /**
69
216
  * Determines if an HTTP error is retryable based on status code
70
217
  * Only network errors and certain HTTP status codes should be retried
@@ -452,12 +599,10 @@ export class ImageProcessor {
452
599
  return "image/webp";
453
600
  }
454
601
  }
455
- // SVG: check for "<svg" or "<?xml" at start (text-based)
456
- if (input.length >= 4) {
457
- const start = input.subarray(0, 4).toString();
458
- if (start === "<svg" || start === "<?xm") {
459
- return "image/svg+xml";
460
- }
602
+ // SVG is text-based and may legally put a BOM, whitespace, an XML
603
+ // declaration, comments, or a doctype before its root element.
604
+ if (hasSvgRoot(input)) {
605
+ return "image/svg+xml";
461
606
  }
462
607
  // ISO-BMFF images: "ftyp" box at offset 4, major brand at offset 8.
463
608
  // AVIF has codec-specific major brands. HEIC uses HEVC image/sequence
@@ -1,4 +1,4 @@
1
- import type { GenerateOptions, MultimodalChatMessage, StreamOptions, TextGenerationOptions } from "../types/index.js";
1
+ import type { GenerateOptions, MultimodalChatMessage, StreamOptions, TextGenerationOptions, VisionImageOutputFormat } from "../types/index.js";
2
2
  import type { ModelMessage } from "../types/index.js";
3
3
  /**
4
4
  * Type-safe conversion from MultimodalChatMessage[] to ModelMessage[]
@@ -56,4 +56,4 @@ export declare function buildMultimodalMessagesArray(options: GenerateOptions, p
56
56
  * Entries that are `http(s)` URLs are left alone — they have no bytes yet, and
57
57
  * `processImageToBase64` converts them once they are downloaded.
58
58
  */
59
- export declare function normalizeVisionImageFormats(input: GenerateOptions["input"]): Promise<void>;
59
+ export declare function normalizeVisionImageFormats(input: GenerateOptions["input"], outputFormat?: VisionImageOutputFormat): Promise<void>;
@@ -1352,7 +1352,7 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1352
1352
  await processUnifiedFilesArray(options, maxSize, provider);
1353
1353
  // Detection can append images (PDF page renders, video keyframes, a HEIC
1354
1354
  // photo), so compatibility conversion has to run after it, not before.
1355
- await normalizeVisionImageFormats(inp);
1355
+ await normalizeVisionImageFormats(inp, options.imageOptions?.outputFormat);
1356
1356
  // Process explicit CSV files array
1357
1357
  await processExplicitCsvFiles(options);
1358
1358
  // Post-processing budget enforcement and token usage logging
@@ -1486,7 +1486,7 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1486
1486
  // the multimodal converter below, so the bytes were dropped and only the
1487
1487
  // metadata summary folded into `text` survived. Exactly the failure this
1488
1488
  // change exists to remove, reintroduced through the other door.
1489
- userContent = await convertContentToProviderFormat(inp.content, provider, model, options.pdfOptions, inp.nativeAudioFiles ?? []);
1489
+ userContent = await convertContentToProviderFormat(inp.content, provider, model, options.pdfOptions, inp.nativeAudioFiles ?? [], options.imageOptions?.outputFormat);
1490
1490
  }
1491
1491
  else if ((inp.images && inp.images.length > 0) ||
1492
1492
  pdfFiles.length > 0 ||
@@ -1495,7 +1495,10 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1495
1495
  // recording was dropped and only its metadata summary — already folded
1496
1496
  // into `text` — ever reached the model.
1497
1497
  (inp.nativeAudioFiles?.length ?? 0) > 0) {
1498
- userContent = await convertMultimodalToProviderFormat(inp.text ?? "", inp.images || [], pdfFiles, provider, model, inp.nativeAudioFiles ?? []);
1498
+ userContent = await convertMultimodalToProviderFormat(inp.text ?? "", inp.images || [], pdfFiles, provider, model, {
1499
+ audioFiles: inp.nativeAudioFiles ?? [],
1500
+ outputFormat: options.imageOptions?.outputFormat,
1501
+ });
1499
1502
  }
1500
1503
  else {
1501
1504
  userContent = inp.text;
@@ -1571,7 +1574,7 @@ async function appendCsvContentToText(csvItems, baseText) {
1571
1574
  /**
1572
1575
  * Convert advanced content format to provider-specific format
1573
1576
  */
1574
- async function convertContentToProviderFormat(content, provider, _model, pdfOptions, audioFiles = []) {
1577
+ async function convertContentToProviderFormat(content, provider, _model, pdfOptions, audioFiles = [], outputFormat) {
1575
1578
  const textContent = content.find((c) => c.type === "text");
1576
1579
  const imageContent = content.filter((c) => c.type === "image");
1577
1580
  const pdfContent = content.filter((c) => c.type === "pdf");
@@ -1624,7 +1627,10 @@ async function convertContentToProviderFormat(content, provider, _model, pdfOpti
1624
1627
  await enforceAggregatePdfLimits(pdfFiles, provider, {
1625
1628
  trustSuppliedPageCounts: false,
1626
1629
  });
1627
- return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model, audioFiles);
1630
+ return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model, {
1631
+ audioFiles,
1632
+ outputFormat,
1633
+ });
1628
1634
  }
1629
1635
  /**
1630
1636
  * Check if a string is an internet URL
@@ -1868,7 +1874,7 @@ async function convertFilePathToBase64(filePath) {
1868
1874
  * Passing raw base64 avoids this because `new URL(base64string)` throws and
1869
1875
  * the SDK treats the string as inline base64 data instead.
1870
1876
  */
1871
- async function processImageToBase64(image, index) {
1877
+ async function processImageToBase64(image, index, outputFormat) {
1872
1878
  let imageData;
1873
1879
  let mimeType = "image/jpeg"; // Default mime type
1874
1880
  if (typeof image === "string") {
@@ -1947,7 +1953,7 @@ async function processImageToBase64(image, index) {
1947
1953
  const context = `image input at index ${index}`;
1948
1954
  ImageProcessor.validateSize(base64DecodedByteLength(imageData), context);
1949
1955
  const rawImage = Buffer.from(imageData, "base64");
1950
- const compatible = await toVisionCompatibleImage(rawImage, mimeType);
1956
+ const compatible = await toVisionCompatibleImage(rawImage, mimeType, outputFormat);
1951
1957
  if (compatible.converted) {
1952
1958
  imageData = compatible.buffer.toString("base64");
1953
1959
  mimeType = compatible.mimeType;
@@ -1968,7 +1974,7 @@ async function processImageToBase64(image, index) {
1968
1974
  * Entries that are `http(s)` URLs are left alone — they have no bytes yet, and
1969
1975
  * `processImageToBase64` converts them once they are downloaded.
1970
1976
  */
1971
- export async function normalizeVisionImageFormats(input) {
1977
+ export async function normalizeVisionImageFormats(input, outputFormat) {
1972
1978
  const images = input?.images;
1973
1979
  if (!images || images.length === 0) {
1974
1980
  return;
@@ -1985,7 +1991,7 @@ export async function normalizeVisionImageFormats(input) {
1985
1991
  if (!source || !needsVisionTranscode(source.mimeType)) {
1986
1992
  continue;
1987
1993
  }
1988
- const compatible = await toVisionCompatibleImage(source.buffer, source.mimeType);
1994
+ const compatible = await toVisionCompatibleImage(source.buffer, source.mimeType, outputFormat);
1989
1995
  if (!compatible.converted) {
1990
1996
  continue;
1991
1997
  }
@@ -2115,7 +2121,7 @@ async function readImageSourceForConversion(payload) {
2115
2121
  * - Buffers/Data URIs: Processed normally
2116
2122
  * - Supports alt text for accessibility (included as context in text parts)
2117
2123
  */
2118
- async function convertSimpleImagesToProviderFormat(text, images, provider, _model) {
2124
+ async function convertSimpleImagesToProviderFormat(text, images, provider, _model, outputFormat) {
2119
2125
  // Validate image count against provider-specific limits before processing
2120
2126
  ProviderImageAdapter.validateImageCount(images.length, provider, _model);
2121
2127
  // For Vercel AI SDK, we need to return the content in the standard format
@@ -2172,7 +2178,7 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
2172
2178
  for (const [index, { data: image }] of actualImages.entries()) {
2173
2179
  try {
2174
2180
  // Use helper function to process image and reduce nesting depth
2175
- const { imageData, mimeType } = await processImageToBase64(image, index);
2181
+ const { imageData, mimeType } = await processImageToBase64(image, index, outputFormat);
2176
2182
  content.push({
2177
2183
  type: "image",
2178
2184
  image: imageData,
@@ -2195,13 +2201,17 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
2195
2201
  async function convertMultimodalToProviderFormat(text, images,
2196
2202
  // The canonical entry shape (#309) rather than a fourth copy of it inline —
2197
2203
  // which is what let the render knobs stop short of this function.
2198
- pdfFiles, provider, model, audioFiles = []) {
2204
+ pdfFiles, provider, model,
2205
+ // Bundled rather than two more positional params: a 7th would trip the
2206
+ // repo's `max-params` ceiling of 6.
2207
+ mediaOptions = {}) {
2208
+ const { audioFiles = [], outputFormat } = mediaOptions;
2199
2209
  const content = [
2200
2210
  { type: "text", text },
2201
2211
  ];
2202
2212
  // Add images if present
2203
2213
  if (images.length > 0) {
2204
- const imageContent = await convertSimpleImagesToProviderFormat("", images, provider, model);
2214
+ const imageContent = await convertSimpleImagesToProviderFormat("", images, provider, model, outputFormat);
2205
2215
  if (Array.isArray(imageContent)) {
2206
2216
  imageContent.forEach((item) => {
2207
2217
  if (item.type !== "text") {
@@ -61,6 +61,9 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
61
61
  scale?: number;
62
62
  maxPages?: number;
63
63
  } | undefined;
64
+ imageOptions: {
65
+ outputFormat?: import("../index.js").VisionImageOutputFormat;
66
+ } | undefined;
64
67
  systemPrompt: string | undefined;
65
68
  conversationHistory: import("../index.js").ChatMessage[] | undefined;
66
69
  provider: string;
@@ -59,6 +59,7 @@ export function buildMultimodalOptions(options, providerName, modelName) {
59
59
  },
60
60
  csvOptions: options.csvOptions,
61
61
  pdfOptions: options.pdfOptions,
62
+ imageOptions: options.imageOptions,
62
63
  systemPrompt: options.systemPrompt,
63
64
  conversationHistory: options.conversationMessages,
64
65
  provider: providerName,