@juspay/neurolink 10.10.11 → 10.11.0

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 (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/adapters/imageFormatSupport.d.ts +75 -0
  3. package/dist/adapters/imageFormatSupport.js +283 -0
  4. package/dist/adapters/video/ffmpegAdapter.d.ts +6 -0
  5. package/dist/adapters/video/ffmpegAdapter.js +1 -1
  6. package/dist/browser/neurolink.min.js +399 -398
  7. package/dist/lib/adapters/imageFormatSupport.d.ts +75 -0
  8. package/dist/lib/adapters/imageFormatSupport.js +284 -0
  9. package/dist/lib/adapters/video/ffmpegAdapter.d.ts +6 -0
  10. package/dist/lib/adapters/video/ffmpegAdapter.js +1 -1
  11. package/dist/lib/processors/config/fileExtensions.d.ts +32 -15
  12. package/dist/lib/processors/config/fileExtensions.js +27 -66
  13. package/dist/lib/processors/config/fileTypeRegistry.d.ts +106 -0
  14. package/dist/lib/processors/config/fileTypeRegistry.js +702 -0
  15. package/dist/lib/processors/config/index.d.ts +2 -1
  16. package/dist/lib/processors/config/index.js +5 -1
  17. package/dist/lib/processors/config/mimeConstants.d.ts +22 -7
  18. package/dist/lib/processors/config/mimeConstants.js +45 -66
  19. package/dist/lib/processors/media/AudioProcessor.js +16 -38
  20. package/dist/lib/processors/media/VideoProcessor.js +11 -32
  21. package/dist/lib/providers/googleAiStudio/client.js +12 -1
  22. package/dist/lib/providers/googleVertex/client.js +123 -66
  23. package/dist/lib/types/file.d.ts +41 -0
  24. package/dist/lib/utils/fileDetector.js +367 -251
  25. package/dist/lib/utils/imageProcessor.js +14 -17
  26. package/dist/lib/utils/markupSniff.d.ts +37 -0
  27. package/dist/lib/utils/markupSniff.js +125 -0
  28. package/dist/lib/utils/messageBuilder.d.ts +14 -0
  29. package/dist/lib/utils/messageBuilder.js +287 -74
  30. package/dist/processors/config/fileExtensions.d.ts +32 -15
  31. package/dist/processors/config/fileExtensions.js +27 -66
  32. package/dist/processors/config/fileTypeRegistry.d.ts +106 -0
  33. package/dist/processors/config/fileTypeRegistry.js +701 -0
  34. package/dist/processors/config/index.d.ts +2 -1
  35. package/dist/processors/config/index.js +5 -1
  36. package/dist/processors/config/mimeConstants.d.ts +22 -7
  37. package/dist/processors/config/mimeConstants.js +45 -66
  38. package/dist/processors/media/AudioProcessor.js +16 -38
  39. package/dist/processors/media/VideoProcessor.js +11 -32
  40. package/dist/providers/googleAiStudio/client.js +12 -1
  41. package/dist/providers/googleVertex/client.js +123 -66
  42. package/dist/types/file.d.ts +41 -0
  43. package/dist/utils/fileDetector.js +367 -251
  44. package/dist/utils/imageProcessor.js +14 -17
  45. package/dist/utils/markupSniff.d.ts +37 -0
  46. package/dist/utils/markupSniff.js +124 -0
  47. package/dist/utils/messageBuilder.d.ts +14 -0
  48. package/dist/utils/messageBuilder.js +287 -74
  49. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [10.11.0](https://github.com/juspay/neurolink/compare/v10.10.12...v10.11.0) (2026-08-10)
2
+
3
+ ### Features
4
+
5
+ - **(files):** support every multimodal format exhaustively via one registry ([a588cca](https://github.com/juspay/neurolink/commit/a588cca4ac89dbcdc2fb1aa0ec256794dbcee3f6))
6
+
7
+ ## [10.10.12](https://github.com/juspay/neurolink/compare/v10.10.11...v10.10.12) (2026-08-10)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **(multimodal):** stop dropping images above the lazy-reference threshold ([85de8f3](https://github.com/juspay/neurolink/commit/85de8f352b8a6f4fa4953042247a1eebaedf1640))
12
+
1
13
  ## [10.10.11](https://github.com/juspay/neurolink/compare/v10.10.10...v10.10.11) (2026-08-08)
2
14
 
3
15
  ### Bug Fixes
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Vision-provider image format compatibility.
3
+ *
4
+ * NeuroLink identifies far more image formats than any vision API accepts. BMP,
5
+ * TIFF, AVIF, ICO and JPEG 2000 are accepted by none of them; HEIC and HEIF are
6
+ * accepted only by Google. Until this module existed those formats were
7
+ * detected correctly, labelled correctly, and then forwarded verbatim — the
8
+ * request reached the provider and came back as an opaque HTTP 400, which is
9
+ * the least useful outcome available: the file was clearly an image, NeuroLink
10
+ * knew exactly which kind, and still nothing worked.
11
+ *
12
+ * A phone photo is the common case. iOS writes HEIC by default, so "attach a
13
+ * photo and ask what is in it" failed for every provider except Google.
14
+ *
15
+ * Anything outside the universal set is transcoded to PNG. PNG rather than JPEG
16
+ * because the sources are frequently lossless (TIFF, BMP, ICO) or already
17
+ * carry alpha, and a lossy re-encode of an image the model is about to read
18
+ * closely is the wrong default.
19
+ *
20
+ * @module adapters/imageFormatSupport
21
+ */
22
+ import type { ImageWithAltText, VisionImageConversion } from "../types/index.js";
23
+ /**
24
+ * MIME types every vision-capable provider accepts as-is.
25
+ *
26
+ * This is the intersection across OpenAI, Anthropic, Google (AI Studio and
27
+ * Vertex), Bedrock, Azure and Mistral — deliberately the intersection and not
28
+ * a per-provider matrix. Google additionally accepts HEIC/HEIF natively, but
29
+ * converting those for Google as well costs one transcode and removes an
30
+ * entire axis of provider-specific branching from the dispatch path.
31
+ */
32
+ export declare const UNIVERSAL_VISION_IMAGE_MIME_TYPES: ReadonlySet<string>;
33
+ /**
34
+ * Every image MIME type NeuroLink accepts as *input*.
35
+ *
36
+ * The union of what providers take as-is and what this module can convert for
37
+ * them. Intake validation must use this rather than the universal set alone:
38
+ * a format we can transcode is a format we accept, and gating intake on the
39
+ * provider-acceptable list rejects the file before conversion ever runs.
40
+ */
41
+ export declare const SUPPORTED_INPUT_IMAGE_MIME_TYPES: ReadonlySet<string>;
42
+ /**
43
+ * True when a MIME type needs transcoding before it can be sent to a vision
44
+ * provider. Cheap enough to call on every image; callers use it to avoid
45
+ * reading a file off disk that would not have been converted anyway.
46
+ */
47
+ export declare function needsVisionTranscode(mimeType: string): boolean;
48
+ /**
49
+ * Return image bytes every vision provider can read, transcoding to PNG when
50
+ * the source format is one no provider accepts.
51
+ *
52
+ * Never throws for image reasons. When neither backend can decode the input,
53
+ * the original bytes are returned with a warning naming the format — the
54
+ * request then fails at the provider exactly as it did before, rather than this
55
+ * compatibility step becoming a new way for a previously working request to
56
+ * break.
57
+ *
58
+ * @param buffer - Raw image bytes.
59
+ * @param mimeType - Detected MIME type of `buffer`.
60
+ */
61
+ export declare function toVisionCompatibleImage(buffer: Buffer, mimeType: string): Promise<VisionImageConversion>;
62
+ /**
63
+ * Unwrap an `input.images` entry to its payload.
64
+ *
65
+ * `ImageWithAltText` (`{ data, altText }`) is a documented public input shape,
66
+ * but the provider image loops typed the array as `Buffer | string` and so
67
+ * treated a wrapper as raw bytes — `toString("base64")` on the object yields
68
+ * the literal "[object Object]", which is valid base64 that decodes to seven
69
+ * bytes of garbage. The request therefore reached the API and failed as
70
+ * "invalid image data", with nothing pointing at the stringification.
71
+ *
72
+ * Alt text has no representation in Vertex's `inlineData` part, so it is
73
+ * dropped here deliberately rather than corrupting the payload to carry it.
74
+ */
75
+ export declare function unwrapImagePayload(entry: Buffer | string | ImageWithAltText): Buffer | string;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Vision-provider image format compatibility.
3
+ *
4
+ * NeuroLink identifies far more image formats than any vision API accepts. BMP,
5
+ * TIFF, AVIF, ICO and JPEG 2000 are accepted by none of them; HEIC and HEIF are
6
+ * accepted only by Google. Until this module existed those formats were
7
+ * detected correctly, labelled correctly, and then forwarded verbatim — the
8
+ * request reached the provider and came back as an opaque HTTP 400, which is
9
+ * the least useful outcome available: the file was clearly an image, NeuroLink
10
+ * knew exactly which kind, and still nothing worked.
11
+ *
12
+ * A phone photo is the common case. iOS writes HEIC by default, so "attach a
13
+ * photo and ask what is in it" failed for every provider except Google.
14
+ *
15
+ * Anything outside the universal set is transcoded to PNG. PNG rather than JPEG
16
+ * because the sources are frequently lossless (TIFF, BMP, ICO) or already
17
+ * carry alpha, and a lossy re-encode of an image the model is about to read
18
+ * closely is the wrong default.
19
+ *
20
+ * @module adapters/imageFormatSupport
21
+ */
22
+ import { extensionForMimeType } from "../processors/config/fileTypeRegistry.js";
23
+ import { withTimeout } from "../utils/errorHandling.js";
24
+ import { logger } from "../utils/logger.js";
25
+ import { tryImport } from "../utils/tryImport.js";
26
+ import { getFfmpegPath, runFfmpeg } from "./video/ffmpegAdapter.js";
27
+ /**
28
+ * Per-backend ceiling for one image conversion.
29
+ *
30
+ * Generous enough for a large TIFF or a HEIC burst frame on a loaded machine,
31
+ * short enough that a hung decoder cannot hold a generation request open.
32
+ */
33
+ const IMAGE_TRANSCODE_TIMEOUT_MS = 30_000;
34
+ /**
35
+ * MIME types every vision-capable provider accepts as-is.
36
+ *
37
+ * This is the intersection across OpenAI, Anthropic, Google (AI Studio and
38
+ * Vertex), Bedrock, Azure and Mistral — deliberately the intersection and not
39
+ * a per-provider matrix. Google additionally accepts HEIC/HEIF natively, but
40
+ * converting those for Google as well costs one transcode and removes an
41
+ * entire axis of provider-specific branching from the dispatch path.
42
+ */
43
+ export const UNIVERSAL_VISION_IMAGE_MIME_TYPES = new Set([
44
+ "image/png",
45
+ "image/jpeg",
46
+ "image/gif",
47
+ "image/webp",
48
+ ]);
49
+ /**
50
+ * Formats that are transcoded to PNG before dispatch.
51
+ *
52
+ * An explicit allowlist rather than "anything not universal": a MIME type this
53
+ * module does not recognise is more likely a mislabelled file than a format
54
+ * sharp can decode, and passing it through unchanged preserves the provider's
55
+ * own error message instead of replacing it with a decode failure here.
56
+ */
57
+ const TRANSCODABLE_IMAGE_MIME_TYPES = new Set([
58
+ "image/bmp",
59
+ "image/x-ms-bmp",
60
+ "image/tiff",
61
+ "image/x-tiff",
62
+ "image/avif",
63
+ "image/heic",
64
+ "image/heic-sequence",
65
+ "image/heif",
66
+ "image/heif-sequence",
67
+ "image/x-icon",
68
+ "image/vnd.microsoft.icon",
69
+ "image/jp2",
70
+ "image/jpx",
71
+ "image/apng",
72
+ // SVG reaches this module only when it arrives as raw bytes in `input.images`
73
+ // rather than through detection (which routes .svg to the sanitizer). No
74
+ // vision provider accepts image/svg+xml, and sharp rasterises SVG natively,
75
+ // so converting is strictly better than shipping markup labelled as an image.
76
+ "image/svg+xml",
77
+ ]);
78
+ /**
79
+ * Every image MIME type NeuroLink accepts as *input*.
80
+ *
81
+ * The union of what providers take as-is and what this module can convert for
82
+ * them. Intake validation must use this rather than the universal set alone:
83
+ * a format we can transcode is a format we accept, and gating intake on the
84
+ * provider-acceptable list rejects the file before conversion ever runs.
85
+ */
86
+ export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = new Set([
87
+ ...UNIVERSAL_VISION_IMAGE_MIME_TYPES,
88
+ ...TRANSCODABLE_IMAGE_MIME_TYPES,
89
+ ]);
90
+ /**
91
+ * True when a MIME type needs transcoding before it can be sent to a vision
92
+ * provider. Cheap enough to call on every image; callers use it to avoid
93
+ * reading a file off disk that would not have been converted anyway.
94
+ */
95
+ export function needsVisionTranscode(mimeType) {
96
+ const normalized = mimeType.split(";")[0].trim().toLowerCase();
97
+ return (!UNIVERSAL_VISION_IMAGE_MIME_TYPES.has(normalized) &&
98
+ TRANSCODABLE_IMAGE_MIME_TYPES.has(normalized));
99
+ }
100
+ /**
101
+ * Decode with sharp. Covers TIFF, AVIF, GIF and SVG in-process with no temp
102
+ * files, which is the fast path.
103
+ *
104
+ * Deliberately does NOT cover every transcodable format: sharp's prebuilt
105
+ * binaries report `heif` as an input format but that is AV1-in-HEIF (AVIF)
106
+ * only — actual HEVC-coded HEIC fails inside libheif, and BMP, ICO and
107
+ * JPEG 2000 are not compiled in at all. Those fall through to ffmpeg below.
108
+ */
109
+ async function transcodeWithSharp(buffer) {
110
+ const sharpModule = await tryImport("sharp", "Image format conversion for vision providers");
111
+ // A shape guard, not a crash guard: an absent or malformed sharp already
112
+ // fails safely, because `tryImport` throws a named install error and any
113
+ // TypeError from calling a non-function is caught by the backend loop, which
114
+ // then tries ffmpeg. What this adds is a legible reason in that loop's
115
+ // failure list instead of "sharpModule.default is not a function".
116
+ //
117
+ // It deliberately throws rather than returning `buffer`: returning the input
118
+ // would report a successful conversion and relabel the original bytes as PNG,
119
+ // and would also skip the ffmpeg backend — the one that actually handles
120
+ // HEIC, BMP, ICO and JPEG 2000.
121
+ if (typeof sharpModule?.default !== "function") {
122
+ throw new Error("the installed sharp package does not expose a callable default export");
123
+ }
124
+ // `pages: 1` keeps a multi-frame source (animated AVIF, a .heics sequence,
125
+ // a multi-page TIFF) from being flattened into one tall strip — the first
126
+ // frame is what a vision model should receive.
127
+ const pipeline = sharpModule.default(buffer, { pages: 1 });
128
+ // The instance shape is checked as well as the factory: a build that exports
129
+ // a callable but returns something without `.png()` would otherwise fail as
130
+ // an opaque TypeError inside the backend loop.
131
+ if (typeof pipeline?.png !== "function") {
132
+ throw new Error("the installed sharp package returned a pipeline without a png() encoder");
133
+ }
134
+ return pipeline.png().toBuffer();
135
+ }
136
+ /**
137
+ * Decode with ffmpeg, which handles what sharp cannot — most importantly HEIC,
138
+ * the format iPhones write by default and therefore the single most common
139
+ * "why can't the model see my photo" case.
140
+ *
141
+ * ffmpeg is already a soft dependency for video keyframe extraction and is
142
+ * resolved through the same `FFMPEG_PATH` → `ffmpeg-static` → system-PATH
143
+ * chain, so this adds a code path rather than a new requirement.
144
+ *
145
+ * Requires temp files: ffmpeg's image demuxers seek, so piping through stdin is
146
+ * not reliable for these formats. The temp directory is removed in `finally`
147
+ * whether or not the conversion succeeded.
148
+ *
149
+ * The Node builtins are imported dynamically rather than at module scope
150
+ * because the browser bundle stubs `node:fs/promises` and its stub has no
151
+ * `mkdtemp`. This whole path is server-only — nothing in a browser is going to
152
+ * spawn ffmpeg — so the import belongs where it is used.
153
+ */
154
+ async function transcodeWithFfmpeg(buffer, extension, binaryPath) {
155
+ const [{ randomUUID }, { mkdtemp, readFile, rm, writeFile }, { tmpdir }, { join },] = await Promise.all([
156
+ import("node:crypto"),
157
+ import("node:fs/promises"),
158
+ import("node:os"),
159
+ import("node:path"),
160
+ ]);
161
+ const workDir = await mkdtemp(join(tmpdir(), "neurolink-img-"));
162
+ const inputPath = join(workDir, `${randomUUID()}${extension}`);
163
+ const outputPath = join(workDir, `${randomUUID()}.png`);
164
+ try {
165
+ await writeFile(inputPath, buffer);
166
+ await runFfmpeg([
167
+ "-y",
168
+ "-v",
169
+ "error",
170
+ "-i",
171
+ inputPath,
172
+ // Take a single frame so a multi-image container yields one PNG rather
173
+ // than ffmpeg erroring on a missing output-sequence pattern.
174
+ "-frames:v",
175
+ "1",
176
+ "-f",
177
+ "image2",
178
+ "-c:v",
179
+ "png",
180
+ outputPath,
181
+ ], binaryPath ? { binaryPath } : {});
182
+ return await readFile(outputPath);
183
+ }
184
+ finally {
185
+ await rm(workDir, { recursive: true, force: true }).catch(() => undefined);
186
+ }
187
+ }
188
+ /**
189
+ * The decode backends to try, in order, cheapest first.
190
+ *
191
+ * The two ffmpeg entries are not redundant. `getFfmpegPath()` prefers the
192
+ * `ffmpeg-static` package, whose LGPL build omits HEVC and therefore cannot
193
+ * read HEIC — the format iPhones write by default. A system ffmpeg usually can,
194
+ * so when the resolved binary is not already the system one it is retried
195
+ * explicitly rather than reporting a photo as unsupported.
196
+ */
197
+ async function* transcodeBackends(buffer, extension) {
198
+ yield { name: "sharp", run: () => transcodeWithSharp(buffer) };
199
+ const resolved = await getFfmpegPath().catch(() => "ffmpeg");
200
+ yield {
201
+ name: "ffmpeg",
202
+ run: () => transcodeWithFfmpeg(buffer, extension),
203
+ };
204
+ if (resolved !== "ffmpeg") {
205
+ yield {
206
+ name: "system ffmpeg",
207
+ run: () => transcodeWithFfmpeg(buffer, extension, "ffmpeg"),
208
+ };
209
+ }
210
+ }
211
+ /**
212
+ * Return image bytes every vision provider can read, transcoding to PNG when
213
+ * the source format is one no provider accepts.
214
+ *
215
+ * Never throws for image reasons. When neither backend can decode the input,
216
+ * the original bytes are returned with a warning naming the format — the
217
+ * request then fails at the provider exactly as it did before, rather than this
218
+ * compatibility step becoming a new way for a previously working request to
219
+ * break.
220
+ *
221
+ * @param buffer - Raw image bytes.
222
+ * @param mimeType - Detected MIME type of `buffer`.
223
+ */
224
+ export async function toVisionCompatibleImage(buffer, mimeType) {
225
+ if (!needsVisionTranscode(mimeType)) {
226
+ return { buffer, mimeType, converted: false };
227
+ }
228
+ const normalized = mimeType.split(";")[0].trim().toLowerCase();
229
+ const extension = extensionForMimeType(normalized) ?? ".bin";
230
+ const failures = [];
231
+ for await (const backend of transcodeBackends(buffer, extension)) {
232
+ try {
233
+ // Both backends can stall — sharp on a malformed stream, ffmpeg on a
234
+ // container it half-understands — and this runs inline on the request
235
+ // path. A bounded failure falls through to the next backend and finally
236
+ // to pass-through, which is the same degradation as a decode error.
237
+ const converted = await withTimeout(backend.run(), IMAGE_TRANSCODE_TIMEOUT_MS, new Error(`${backend.name} image transcode exceeded ${IMAGE_TRANSCODE_TIMEOUT_MS}ms`));
238
+ if (converted.length === 0) {
239
+ throw new Error("produced an empty image");
240
+ }
241
+ logger.debug(`[imageFormatSupport] Transcoded ${normalized} → image/png via ${backend.name} ` +
242
+ `(${buffer.length} → ${converted.length} bytes) for vision compatibility`);
243
+ return { buffer: converted, mimeType: "image/png", converted: true };
244
+ }
245
+ catch (error) {
246
+ failures.push(`${backend.name}: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`);
247
+ }
248
+ }
249
+ logger.warn(`[imageFormatSupport] Could not transcode ${normalized} to PNG — sending the ` +
250
+ `original bytes, which most vision providers will reject. Install a full ` +
251
+ `ffmpeg build (or set FFMPEG_PATH to one) to enable this format. ` +
252
+ `Tried ${failures.join("; ")}`);
253
+ return { buffer, mimeType, converted: false };
254
+ }
255
+ /**
256
+ * Whether an `input.images` entry is the `{ data, altText }` wrapper.
257
+ *
258
+ * Narrowing on the `data` property rather than on `typeof entry === "object"`:
259
+ * a Buffer is also an object, so the looser test leaves `ImageWithAltText` in
260
+ * the union on the false branch and only compiles behind a cast.
261
+ */
262
+ function isImageWithAltTextEntry(entry) {
263
+ return (typeof entry === "object" &&
264
+ entry !== null &&
265
+ !Buffer.isBuffer(entry) &&
266
+ "data" in entry);
267
+ }
268
+ /**
269
+ * Unwrap an `input.images` entry to its payload.
270
+ *
271
+ * `ImageWithAltText` (`{ data, altText }`) is a documented public input shape,
272
+ * but the provider image loops typed the array as `Buffer | string` and so
273
+ * treated a wrapper as raw bytes — `toString("base64")` on the object yields
274
+ * the literal "[object Object]", which is valid base64 that decodes to seven
275
+ * bytes of garbage. The request therefore reached the API and failed as
276
+ * "invalid image data", with nothing pointing at the stringification.
277
+ *
278
+ * Alt text has no representation in Vertex's `inlineData` part, so it is
279
+ * dropped here deliberately rather than corrupting the payload to carry it.
280
+ */
281
+ export function unwrapImagePayload(entry) {
282
+ return isImageWithAltTextEntry(entry) ? entry.data : entry;
283
+ }
@@ -62,6 +62,12 @@ export declare function getFfmpegPath(): Promise<string>;
62
62
  export declare function runFfmpeg(args: string[], options?: {
63
63
  timeoutMs?: number;
64
64
  maxBuffer?: number;
65
+ /**
66
+ * Override the resolved binary. Used to retry against the system `ffmpeg`
67
+ * when the `ffmpeg-static` build lacks a codec — its LGPL build ships
68
+ * without HEVC, so it cannot decode HEIC even though a system build can.
69
+ */
70
+ binaryPath?: string;
65
71
  }): Promise<{
66
72
  stdout: string;
67
73
  stderr: string;
@@ -167,7 +167,7 @@ export async function getFfmpegPath() {
167
167
  */
168
168
  export async function runFfmpeg(args, options = {}) {
169
169
  const { execFile } = await import("node:child_process");
170
- const ffmpegPath = await getFfmpegPath();
170
+ const ffmpegPath = options.binaryPath ?? (await getFfmpegPath());
171
171
  const timeoutMs = options.timeoutMs ?? FFMPEG_FRAME_TIMEOUT_MS;
172
172
  const maxBuffer = options.maxBuffer ?? FFMPEG_FRAME_MAX_BUFFER;
173
173
  return new Promise((resolve, reject) => {