@juspay/neurolink 10.10.12 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/adapters/imageFormatSupport.d.ts +75 -0
- package/dist/adapters/imageFormatSupport.js +283 -0
- package/dist/adapters/video/ffmpegAdapter.d.ts +6 -0
- package/dist/adapters/video/ffmpegAdapter.js +1 -1
- package/dist/browser/neurolink.min.js +399 -398
- package/dist/lib/adapters/imageFormatSupport.d.ts +75 -0
- package/dist/lib/adapters/imageFormatSupport.js +284 -0
- package/dist/lib/adapters/video/ffmpegAdapter.d.ts +6 -0
- package/dist/lib/adapters/video/ffmpegAdapter.js +1 -1
- package/dist/lib/processors/config/fileExtensions.d.ts +32 -15
- package/dist/lib/processors/config/fileExtensions.js +27 -66
- package/dist/lib/processors/config/fileTypeRegistry.d.ts +106 -0
- package/dist/lib/processors/config/fileTypeRegistry.js +702 -0
- package/dist/lib/processors/config/index.d.ts +2 -1
- package/dist/lib/processors/config/index.js +5 -1
- package/dist/lib/processors/config/mimeConstants.d.ts +22 -7
- package/dist/lib/processors/config/mimeConstants.js +45 -66
- package/dist/lib/processors/media/AudioProcessor.js +16 -38
- package/dist/lib/processors/media/VideoProcessor.js +11 -32
- package/dist/lib/providers/googleAiStudio/client.js +12 -1
- package/dist/lib/providers/googleVertex/client.js +123 -66
- package/dist/lib/types/file.d.ts +41 -0
- package/dist/lib/utils/fileDetector.js +367 -251
- package/dist/lib/utils/imageProcessor.js +14 -17
- package/dist/lib/utils/markupSniff.d.ts +37 -0
- package/dist/lib/utils/markupSniff.js +125 -0
- package/dist/lib/utils/messageBuilder.d.ts +14 -0
- package/dist/lib/utils/messageBuilder.js +172 -92
- package/dist/processors/config/fileExtensions.d.ts +32 -15
- package/dist/processors/config/fileExtensions.js +27 -66
- package/dist/processors/config/fileTypeRegistry.d.ts +106 -0
- package/dist/processors/config/fileTypeRegistry.js +701 -0
- package/dist/processors/config/index.d.ts +2 -1
- package/dist/processors/config/index.js +5 -1
- package/dist/processors/config/mimeConstants.d.ts +22 -7
- package/dist/processors/config/mimeConstants.js +45 -66
- package/dist/processors/media/AudioProcessor.js +16 -38
- package/dist/processors/media/VideoProcessor.js +11 -32
- package/dist/providers/googleAiStudio/client.js +12 -1
- package/dist/providers/googleVertex/client.js +123 -66
- package/dist/types/file.d.ts +41 -0
- package/dist/utils/fileDetector.js +367 -251
- package/dist/utils/imageProcessor.js +14 -17
- package/dist/utils/markupSniff.d.ts +37 -0
- package/dist/utils/markupSniff.js +124 -0
- package/dist/utils/messageBuilder.d.ts +14 -0
- package/dist/utils/messageBuilder.js +172 -92
- package/package.json +3 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Handles format conversion for different AI providers
|
|
4
4
|
*/
|
|
5
5
|
import { basename } from "path";
|
|
6
|
+
import { SUPPORTED_INPUT_IMAGE_MIME_TYPES } from "../adapters/imageFormatSupport.js";
|
|
6
7
|
import { logger } from "./logger.js";
|
|
7
8
|
import { redactPathFromMessage, redactUrlForError, redactUrlsInText, sanitizeErrorCause, } from "./logSanitize.js";
|
|
8
9
|
import { urlDownloadRateLimiter } from "./rateLimiter.js";
|
|
@@ -565,23 +566,19 @@ export class ImageProcessor {
|
|
|
565
566
|
* Validate image format
|
|
566
567
|
*/
|
|
567
568
|
static validateImageFormat(mediaType) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
// process() must fail loud instead of packaging it as a valid image
|
|
582
|
-
// (see the octet-stream guard in process() below).
|
|
583
|
-
];
|
|
584
|
-
return supportedFormats.includes(mediaType.toLowerCase());
|
|
569
|
+
// Derived from the canonical input set rather than hand-listed. The old
|
|
570
|
+
// literal omitted HEIC, HEIF, ICO and JPEG 2000, so an iPhone photo
|
|
571
|
+
// attached by path was rejected here — "Invalid MIME type: image/heic is
|
|
572
|
+
// not in allowed list" — before the transcode that exists to accept it
|
|
573
|
+
// could run. Intake has to allow every format we can convert, not just the
|
|
574
|
+
// ones providers take unchanged.
|
|
575
|
+
//
|
|
576
|
+
// Still excludes "application/octet-stream": that is detectImageType()'s
|
|
577
|
+
// honest sentinel for bytes matching no known image signature (#261), not
|
|
578
|
+
// a real format. Vision providers reject it with an HTTP 400, so process()
|
|
579
|
+
// must fail loud rather than package it as a valid image (see the
|
|
580
|
+
// octet-stream guard in process() below).
|
|
581
|
+
return SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(mediaType.toLowerCase());
|
|
585
582
|
}
|
|
586
583
|
/**
|
|
587
584
|
* Get image dimensions from Buffer (basic implementation)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content sniffing for text-shaped image formats.
|
|
3
|
+
*
|
|
4
|
+
* SVG is markup, so it has no byte signature — but it does have an unambiguous
|
|
5
|
+
* root element, and identifying it matters twice over: the detector routes SVG
|
|
6
|
+
* to the sanitizer rather than to a vision API, and the image path must not let
|
|
7
|
+
* SVG bytes travel under some other format's MIME type.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is a scan and not a regular expression
|
|
10
|
+
*
|
|
11
|
+
* The obvious implementation strips the prolog with a pattern like
|
|
12
|
+
* `(?:<!--[\s\S]*?-->\s*)*<svg`. That is two nested quantifiers, and CodeQL
|
|
13
|
+
* rightly flags it as `js/redos`: input beginning `<!--` and repeating
|
|
14
|
+
* `--><!--` drives exponential backtracking, so a hostile (or merely odd)
|
|
15
|
+
* upload can burn CPU inside what is supposed to be a cheap type check.
|
|
16
|
+
*
|
|
17
|
+
* The `.replace()`-chain alternative is unsound for a different reason —
|
|
18
|
+
* removing a multi-character construct can join the surrounding text into a
|
|
19
|
+
* fresh instance of that same construct, so one pass does not converge
|
|
20
|
+
* (`<!--<!-- -->-->` leaves a comment behind). CodeQL flags that shape too, as
|
|
21
|
+
* `js/incomplete-multi-character-sanitization`.
|
|
22
|
+
*
|
|
23
|
+
* A single forward scan has neither problem: the cursor only ever advances, so
|
|
24
|
+
* the work is linear in the input and no construct can re-form behind it.
|
|
25
|
+
*
|
|
26
|
+
* @module utils/markupSniff
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Whether the leading bytes are an SVG document.
|
|
30
|
+
*
|
|
31
|
+
* Requires `<svg` to be the *root element* rather than merely present, so an
|
|
32
|
+
* HTML page embedding an inline `<svg>` icon is not mistaken for a standalone
|
|
33
|
+
* image. A BOM, XML declaration, DOCTYPE and comments may precede it.
|
|
34
|
+
*
|
|
35
|
+
* @param input - Buffer or string to inspect; only the head is read.
|
|
36
|
+
*/
|
|
37
|
+
export declare function looksLikeSvgMarkup(input: Buffer | string): boolean;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content sniffing for text-shaped image formats.
|
|
3
|
+
*
|
|
4
|
+
* SVG is markup, so it has no byte signature — but it does have an unambiguous
|
|
5
|
+
* root element, and identifying it matters twice over: the detector routes SVG
|
|
6
|
+
* to the sanitizer rather than to a vision API, and the image path must not let
|
|
7
|
+
* SVG bytes travel under some other format's MIME type.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is a scan and not a regular expression
|
|
10
|
+
*
|
|
11
|
+
* The obvious implementation strips the prolog with a pattern like
|
|
12
|
+
* `(?:<!--[\s\S]*?-->\s*)*<svg`. That is two nested quantifiers, and CodeQL
|
|
13
|
+
* rightly flags it as `js/redos`: input beginning `<!--` and repeating
|
|
14
|
+
* `--><!--` drives exponential backtracking, so a hostile (or merely odd)
|
|
15
|
+
* upload can burn CPU inside what is supposed to be a cheap type check.
|
|
16
|
+
*
|
|
17
|
+
* The `.replace()`-chain alternative is unsound for a different reason —
|
|
18
|
+
* removing a multi-character construct can join the surrounding text into a
|
|
19
|
+
* fresh instance of that same construct, so one pass does not converge
|
|
20
|
+
* (`<!--<!-- -->-->` leaves a comment behind). CodeQL flags that shape too, as
|
|
21
|
+
* `js/incomplete-multi-character-sanitization`.
|
|
22
|
+
*
|
|
23
|
+
* A single forward scan has neither problem: the cursor only ever advances, so
|
|
24
|
+
* the work is linear in the input and no construct can re-form behind it.
|
|
25
|
+
*
|
|
26
|
+
* @module utils/markupSniff
|
|
27
|
+
*/
|
|
28
|
+
/** Bytes inspected when sniffing for a markup root element. */
|
|
29
|
+
const MARKUP_SNIFF_LIMIT = 1024;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the leading bytes are an SVG document.
|
|
32
|
+
*
|
|
33
|
+
* Requires `<svg` to be the *root element* rather than merely present, so an
|
|
34
|
+
* HTML page embedding an inline `<svg>` icon is not mistaken for a standalone
|
|
35
|
+
* image. A BOM, XML declaration, DOCTYPE and comments may precede it.
|
|
36
|
+
*
|
|
37
|
+
* @param input - Buffer or string to inspect; only the head is read.
|
|
38
|
+
*/
|
|
39
|
+
export function looksLikeSvgMarkup(input) {
|
|
40
|
+
const head = typeof input === "string"
|
|
41
|
+
? input.slice(0, MARKUP_SNIFF_LIMIT)
|
|
42
|
+
: input.toString("utf8", 0, Math.min(input.length, MARKUP_SNIFF_LIMIT));
|
|
43
|
+
if (!head.includes("<svg")) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
let cursor = head.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
47
|
+
while (cursor < head.length) {
|
|
48
|
+
while (cursor < head.length && /\s/.test(head[cursor])) {
|
|
49
|
+
cursor++;
|
|
50
|
+
}
|
|
51
|
+
if (head[cursor] !== "<") {
|
|
52
|
+
// Text before any element — not a well-formed XML document.
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// Each prolog construct ends at a known delimiter; an unterminated one
|
|
56
|
+
// means the document is truncated or malformed, so stop rather than guess.
|
|
57
|
+
let end;
|
|
58
|
+
if (head.startsWith("<?", cursor)) {
|
|
59
|
+
end = head.indexOf("?>", cursor + 2);
|
|
60
|
+
cursor = end === -1 ? head.length : end + 2;
|
|
61
|
+
}
|
|
62
|
+
else if (head.startsWith("<!--", cursor)) {
|
|
63
|
+
end = head.indexOf("-->", cursor + 4);
|
|
64
|
+
cursor = end === -1 ? head.length : end + 3;
|
|
65
|
+
}
|
|
66
|
+
else if (head.startsWith("<!", cursor)) {
|
|
67
|
+
// DOCTYPE. Stopping at the first '>' is wrong for two legal forms: a
|
|
68
|
+
// quoted public/system identifier may contain '>', and an internal
|
|
69
|
+
// subset (`<!DOCTYPE svg [ <!ENTITY x "y"> ]>`) certainly does. Either
|
|
70
|
+
// one left the cursor mid-declaration and made a valid SVG classify as
|
|
71
|
+
// not-SVG. Track quotes and subset depth to find the real terminator.
|
|
72
|
+
cursor = skipDoctype(head, cursor);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// First real element decides.
|
|
76
|
+
return /^<svg[\s>/]/i.test(head.slice(cursor));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Return the index just past a DOCTYPE declaration beginning at `start`.
|
|
83
|
+
*
|
|
84
|
+
* Handles quoted identifiers and a bracketed internal subset. Returns
|
|
85
|
+
* `head.length` when the declaration is unterminated within the sniffed
|
|
86
|
+
* window, which stops the caller rather than letting it guess.
|
|
87
|
+
*/
|
|
88
|
+
function skipDoctype(head, start) {
|
|
89
|
+
let quote = null;
|
|
90
|
+
let subsetDepth = 0;
|
|
91
|
+
for (let i = start + 2; i < head.length; i++) {
|
|
92
|
+
const ch = head[i];
|
|
93
|
+
if (quote) {
|
|
94
|
+
if (ch === quote) {
|
|
95
|
+
quote = null;
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// A comment inside the internal subset may itself contain ']' and '>'.
|
|
100
|
+
// Treating those as structural closed the subset early and terminated the
|
|
101
|
+
// DOCTYPE on the comment's own '-->', so valid SVG classified as not-SVG.
|
|
102
|
+
if (head.startsWith("<!--", i)) {
|
|
103
|
+
const commentEnd = head.indexOf("-->", i + 4);
|
|
104
|
+
if (commentEnd === -1) {
|
|
105
|
+
return head.length;
|
|
106
|
+
}
|
|
107
|
+
i = commentEnd + 2; // loop increment steps past the final '>'
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (ch === '"' || ch === "'") {
|
|
111
|
+
quote = ch;
|
|
112
|
+
}
|
|
113
|
+
else if (ch === "[") {
|
|
114
|
+
subsetDepth++;
|
|
115
|
+
}
|
|
116
|
+
else if (ch === "]") {
|
|
117
|
+
subsetDepth = Math.max(0, subsetDepth - 1);
|
|
118
|
+
}
|
|
119
|
+
else if (ch === ">" && subsetDepth === 0) {
|
|
120
|
+
return i + 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return head.length;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=markupSniff.js.map
|
|
@@ -52,3 +52,17 @@ export declare function processUnifiedFilesArray(options: GenerateOptions, maxSi
|
|
|
52
52
|
* Detects when images are present and routes through provider adapter
|
|
53
53
|
*/
|
|
54
54
|
export declare function buildMultimodalMessagesArray(options: GenerateOptions, provider: string, model: string): Promise<MultimodalChatMessage[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Transcode any image in `input.images` that no vision provider accepts,
|
|
57
|
+
* rewriting the entry in place as a PNG data URI.
|
|
58
|
+
*
|
|
59
|
+
* Mutates in place and is idempotent, matching `processUnifiedFilesArray` and
|
|
60
|
+
* `foldMediaAliasesIntoFiles`: the shared multimodal builder and the providers
|
|
61
|
+
* that bypass it (Google AI Studio's and Vertex's native SDK paths, Bedrock's
|
|
62
|
+
* Converse path) can all call it on the same options object without converting
|
|
63
|
+
* anything twice.
|
|
64
|
+
*
|
|
65
|
+
* Entries that are `http(s)` URLs are left alone — they have no bytes yet, and
|
|
66
|
+
* `processImageToBase64` converts them once they are downloaded.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeVisionImageFormats(input: GenerateOptions["input"]): Promise<void>;
|
|
@@ -8,11 +8,16 @@ import { PDF_LIMITS } from "../core/constants.js";
|
|
|
8
8
|
import { enforceAggregateFileBudget, FILE_READ_BUDGET_PERCENT, } from "../context/fileTokenBudget.js";
|
|
9
9
|
import { isCSVContent, SIZE_TIER_THRESHOLDS } from "../types/index.js";
|
|
10
10
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
11
|
+
import { needsVisionTranscode, toVisionCompatibleImage, } from "../adapters/imageFormatSupport.js";
|
|
12
|
+
import { FILE_TYPE_REGISTRY, lookupByExtension, } from "../processors/config/fileTypeRegistry.js";
|
|
11
13
|
import { ErrorFactory, NeuroLinkError, withTimeout } from "./errorHandling.js";
|
|
12
14
|
import { FileDetector } from "./fileDetector.js";
|
|
15
|
+
import { detectIsoBmffImageMimeType } from "./isoBmff.js";
|
|
13
16
|
import { getImageCache } from "./imageCache.js";
|
|
14
17
|
import { ImageProcessor, imageUtils } from "./imageProcessor.js";
|
|
15
18
|
import { logger } from "./logger.js";
|
|
19
|
+
import { looksLikeSvgMarkup } from "./markupSniff.js";
|
|
20
|
+
import { redactUrlForError } from "./logSanitize.js";
|
|
16
21
|
import { PDFImageConverter, PDFProcessor } from "./pdfProcessor.js";
|
|
17
22
|
import { urlDownloadRateLimiter } from "./rateLimiter.js";
|
|
18
23
|
import { estimateTokens } from "./tokenEstimation.js";
|
|
@@ -22,82 +27,16 @@ import { estimateTokens } from "./tokenEstimation.js";
|
|
|
22
27
|
// classify files into broad categories (video, audio, image, etc.) so
|
|
23
28
|
// estimatePostProcessingTokens() can use type-aware estimates.
|
|
24
29
|
// ---------------------------------------------------------------------------
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
m4v: "video",
|
|
36
|
-
// Audio
|
|
37
|
-
mp3: "audio",
|
|
38
|
-
wav: "audio",
|
|
39
|
-
ogg: "audio",
|
|
40
|
-
flac: "audio",
|
|
41
|
-
m4a: "audio",
|
|
42
|
-
aac: "audio",
|
|
43
|
-
wma: "audio",
|
|
44
|
-
opus: "audio",
|
|
45
|
-
// Image
|
|
46
|
-
jpg: "image",
|
|
47
|
-
jpeg: "image",
|
|
48
|
-
jpe: "image",
|
|
49
|
-
jfif: "image",
|
|
50
|
-
png: "image",
|
|
51
|
-
apng: "image",
|
|
52
|
-
gif: "image",
|
|
53
|
-
webp: "image",
|
|
54
|
-
bmp: "image",
|
|
55
|
-
tiff: "image",
|
|
56
|
-
tif: "image",
|
|
57
|
-
avif: "image",
|
|
58
|
-
// Below this line the entries exist because this map now decides whether an
|
|
59
|
-
// image is processed eagerly (see isEagerMultimodalFile), not merely how its
|
|
60
|
-
// tokens are estimated. An extension missing here classifies as `undefined`
|
|
61
|
-
// and takes the lazy path, which drops the pixels — so a gap is a dropped
|
|
62
|
-
// photo, not a slightly-off estimate. HEIC is the one that matters most:
|
|
63
|
-
// it is what an iPhone writes by default.
|
|
64
|
-
heic: "image",
|
|
65
|
-
heif: "image",
|
|
66
|
-
ico: "image",
|
|
67
|
-
jp2: "image",
|
|
68
|
-
jpx: "image",
|
|
69
|
-
// NB: `svg` is deliberately absent here — it is mapped to its own "svg"
|
|
70
|
-
// routing type further down, and listing it twice is a duplicate key whose
|
|
71
|
-
// second entry silently wins. `isEagerMultimodalFile` accepts both types
|
|
72
|
-
// instead, so SVG takes the eager path without this map having to lie about
|
|
73
|
-
// which processor handles it.
|
|
74
|
-
// Archive
|
|
75
|
-
zip: "archive",
|
|
76
|
-
tar: "archive",
|
|
77
|
-
gz: "archive",
|
|
78
|
-
tgz: "archive",
|
|
79
|
-
rar: "archive",
|
|
80
|
-
"7z": "archive",
|
|
81
|
-
jar: "archive",
|
|
82
|
-
// Documents
|
|
83
|
-
xlsx: "xlsx",
|
|
84
|
-
xls: "xlsx",
|
|
85
|
-
ods: "xlsx",
|
|
86
|
-
docx: "docx",
|
|
87
|
-
doc: "docx",
|
|
88
|
-
odt: "docx",
|
|
89
|
-
rtf: "docx",
|
|
90
|
-
pptx: "pptx",
|
|
91
|
-
ppt: "pptx",
|
|
92
|
-
odp: "pptx",
|
|
93
|
-
// PDF
|
|
94
|
-
pdf: "pdf",
|
|
95
|
-
// SVG
|
|
96
|
-
svg: "svg",
|
|
97
|
-
// CSV
|
|
98
|
-
csv: "csv",
|
|
99
|
-
tsv: "csv",
|
|
100
|
-
};
|
|
30
|
+
/**
|
|
31
|
+
* Extension → file type for budget estimation.
|
|
32
|
+
*
|
|
33
|
+
* Derived from the canonical registry rather than hand-listed: this was a
|
|
34
|
+
* fourth copy of the same knowledge, and it disagreed with the detector it is
|
|
35
|
+
* supposed to predict — it had no entry for .mpg/.mpeg/.3gp/.aiff and so on, so
|
|
36
|
+
* estimatePostProcessingTokens() silently fell back to a generic estimate for
|
|
37
|
+
* exactly the large media files whose estimate matters most.
|
|
38
|
+
*/
|
|
39
|
+
const EXTENSION_TYPE_MAP = Object.fromEntries(FILE_TYPE_REGISTRY.flatMap((entry) => entry.extensions.map((ext) => [ext.slice(1), entry.fileType])));
|
|
101
40
|
/**
|
|
102
41
|
* Infer file type from extension in a file path or URL.
|
|
103
42
|
* Returns undefined if no extension or unrecognized.
|
|
@@ -1265,6 +1204,9 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1265
1204
|
enforceFileBudget(options, provider, model);
|
|
1266
1205
|
// Process unified files array (auto-detect)
|
|
1267
1206
|
await processUnifiedFilesArray(options, maxSize, provider);
|
|
1207
|
+
// Detection can append images (PDF page renders, video keyframes, a HEIC
|
|
1208
|
+
// photo), so compatibility conversion has to run after it, not before.
|
|
1209
|
+
await normalizeVisionImageFormats(inp);
|
|
1268
1210
|
// Process explicit CSV files array
|
|
1269
1211
|
await processExplicitCsvFiles(options);
|
|
1270
1212
|
// Post-processing budget enforcement and token usage logging
|
|
@@ -1576,25 +1518,24 @@ async function downloadImageFromUrl(url) {
|
|
|
1576
1518
|
}
|
|
1577
1519
|
}
|
|
1578
1520
|
/**
|
|
1579
|
-
* Get MIME type from file extension
|
|
1521
|
+
* Get MIME type from an image file extension.
|
|
1522
|
+
*
|
|
1523
|
+
* Delegates to the canonical registry instead of the six-case switch this used
|
|
1524
|
+
* to be. That switch covered png/gif/webp/bmp/tiff and defaulted *everything
|
|
1525
|
+
* else* to image/jpeg, so a .heic, .avif, .ico or .jp2 path was labelled JPEG
|
|
1526
|
+
* — which meant `needsVisionTranscode()` never fired for it and the raw bytes
|
|
1527
|
+
* went to the provider under a MIME type that was simply untrue. Exactly the
|
|
1528
|
+
* kind of hand-maintained table this registry exists to delete.
|
|
1529
|
+
*
|
|
1530
|
+
* The image/jpeg fallback is kept only for genuinely unknown extensions, since
|
|
1531
|
+
* callers here have already established they are handling an image.
|
|
1580
1532
|
*/
|
|
1581
1533
|
function getMimeTypeFromExtension(filePath) {
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
return "image/png";
|
|
1586
|
-
case "gif":
|
|
1587
|
-
return "image/gif";
|
|
1588
|
-
case "webp":
|
|
1589
|
-
return "image/webp";
|
|
1590
|
-
case "bmp":
|
|
1591
|
-
return "image/bmp";
|
|
1592
|
-
case "tiff":
|
|
1593
|
-
case "tif":
|
|
1594
|
-
return "image/tiff";
|
|
1595
|
-
default:
|
|
1596
|
-
return "image/jpeg";
|
|
1534
|
+
const entry = lookupByExtension(filePath);
|
|
1535
|
+
if (entry?.modality === "image") {
|
|
1536
|
+
return entry.mimeTypes[0];
|
|
1597
1537
|
}
|
|
1538
|
+
return "image/jpeg";
|
|
1598
1539
|
}
|
|
1599
1540
|
/**
|
|
1600
1541
|
* Detect MIME type from buffer magic bytes
|
|
@@ -1658,6 +1599,45 @@ function detectMimeTypeFromBuffer(buffer) {
|
|
|
1658
1599
|
buffer[3] === 0x2a))) {
|
|
1659
1600
|
return "image/tiff";
|
|
1660
1601
|
}
|
|
1602
|
+
// The formats above are the ones a provider accepts (or that sharp handles
|
|
1603
|
+
// trivially). Everything below is a format NO vision provider accepts, and
|
|
1604
|
+
// recognising it here is what lets `needsVisionTranscode()` fire — a Buffer
|
|
1605
|
+
// whose format was unrecognised fell through to the image/jpeg default and
|
|
1606
|
+
// was shipped to the provider unconverted under a MIME type that was false.
|
|
1607
|
+
// Raw Buffers are the most direct way a backend attaches an image, so this
|
|
1608
|
+
// was the single biggest hole in the vision-compatibility path.
|
|
1609
|
+
// HEIC / HEIF / AVIF: ISO-BMFF `ftyp` box, distinguished by major brand.
|
|
1610
|
+
// Shared with the FileDetector so the two cannot drift on brand tables.
|
|
1611
|
+
const isoBmffImage = detectIsoBmffImageMimeType(buffer);
|
|
1612
|
+
if (isoBmffImage) {
|
|
1613
|
+
return isoBmffImage;
|
|
1614
|
+
}
|
|
1615
|
+
// ICO: 00 00 01 00. Checked after the ftyp probe because an ISO-BMFF file
|
|
1616
|
+
// also starts with a zero byte.
|
|
1617
|
+
if (buffer.length >= 4 &&
|
|
1618
|
+
buffer[0] === 0x00 &&
|
|
1619
|
+
buffer[1] === 0x00 &&
|
|
1620
|
+
buffer[2] === 0x01 &&
|
|
1621
|
+
buffer[3] === 0x00) {
|
|
1622
|
+
return "image/x-icon";
|
|
1623
|
+
}
|
|
1624
|
+
// SVG is markup, so it has no byte signature — and without this probe a raw
|
|
1625
|
+
// SVG buffer matched nothing, kept processImageToBase64's "image/jpeg"
|
|
1626
|
+
// default, and was shipped to the provider as XML labelled as a JPEG.
|
|
1627
|
+
// Shared forward scan rather than a local regex: the regex form of this
|
|
1628
|
+
// check is a CodeQL js/redos finding (see markupSniff).
|
|
1629
|
+
if (looksLikeSvgMarkup(buffer)) {
|
|
1630
|
+
return "image/svg+xml";
|
|
1631
|
+
}
|
|
1632
|
+
// JPEG 2000: 12-byte signature box "....jP ".
|
|
1633
|
+
if (buffer.length >= 8 &&
|
|
1634
|
+
buffer[0] === 0x00 &&
|
|
1635
|
+
buffer[1] === 0x00 &&
|
|
1636
|
+
buffer[2] === 0x00 &&
|
|
1637
|
+
buffer[3] === 0x0c &&
|
|
1638
|
+
buffer.toString("latin1", 4, 8) === "jP ") {
|
|
1639
|
+
return "image/jp2";
|
|
1640
|
+
}
|
|
1661
1641
|
return undefined;
|
|
1662
1642
|
}
|
|
1663
1643
|
/**
|
|
@@ -1764,8 +1744,108 @@ async function processImageToBase64(image, index) {
|
|
|
1764
1744
|
ImageProcessor.validateBufferSize(image, `image input at index ${index}`);
|
|
1765
1745
|
imageData = image.toString("base64");
|
|
1766
1746
|
}
|
|
1747
|
+
// Last line of defence for vision-format compatibility. `normalizeVisionImageFormats`
|
|
1748
|
+
// handles `input.images` eagerly so the providers that read that array
|
|
1749
|
+
// directly (Google AI Studio, Bedrock) see converted bytes, but images that
|
|
1750
|
+
// arrive as URLs are downloaded further downstream and only become bytes
|
|
1751
|
+
// here. A no-op for the universal formats, so the common path is unaffected.
|
|
1752
|
+
if (needsVisionTranscode(mimeType)) {
|
|
1753
|
+
const compatible = await toVisionCompatibleImage(Buffer.from(imageData, "base64"), mimeType);
|
|
1754
|
+
if (compatible.converted) {
|
|
1755
|
+
imageData = compatible.buffer.toString("base64");
|
|
1756
|
+
mimeType = compatible.mimeType;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1767
1759
|
return { imageData, mimeType };
|
|
1768
1760
|
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Transcode any image in `input.images` that no vision provider accepts,
|
|
1763
|
+
* rewriting the entry in place as a PNG data URI.
|
|
1764
|
+
*
|
|
1765
|
+
* Mutates in place and is idempotent, matching `processUnifiedFilesArray` and
|
|
1766
|
+
* `foldMediaAliasesIntoFiles`: the shared multimodal builder and the providers
|
|
1767
|
+
* that bypass it (Google AI Studio's and Vertex's native SDK paths, Bedrock's
|
|
1768
|
+
* Converse path) can all call it on the same options object without converting
|
|
1769
|
+
* anything twice.
|
|
1770
|
+
*
|
|
1771
|
+
* Entries that are `http(s)` URLs are left alone — they have no bytes yet, and
|
|
1772
|
+
* `processImageToBase64` converts them once they are downloaded.
|
|
1773
|
+
*/
|
|
1774
|
+
export async function normalizeVisionImageFormats(input) {
|
|
1775
|
+
const images = input?.images;
|
|
1776
|
+
if (!images || images.length === 0) {
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
for (let index = 0; index < images.length; index++) {
|
|
1780
|
+
const entry = images[index];
|
|
1781
|
+
// ImageWithAltText wraps the payload in `.data`; convert that and keep the
|
|
1782
|
+
// alt text attached rather than dropping the wrapper.
|
|
1783
|
+
const isWrapped = typeof entry === "object" && entry !== null && !Buffer.isBuffer(entry);
|
|
1784
|
+
const payload = isWrapped
|
|
1785
|
+
? entry.data
|
|
1786
|
+
: entry;
|
|
1787
|
+
const source = await readImageSourceForConversion(payload);
|
|
1788
|
+
if (!source || !needsVisionTranscode(source.mimeType)) {
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
const compatible = await toVisionCompatibleImage(source.buffer, source.mimeType);
|
|
1792
|
+
if (!compatible.converted) {
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const dataUri = `data:${compatible.mimeType};base64,${compatible.buffer.toString("base64")}`;
|
|
1796
|
+
images[index] = isWrapped
|
|
1797
|
+
? { ...entry, data: dataUri }
|
|
1798
|
+
: dataUri;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Resolve one `input.images` entry to bytes plus a MIME type, or undefined when
|
|
1803
|
+
* it cannot be resolved without a network call.
|
|
1804
|
+
*
|
|
1805
|
+
* File paths are only read when their extension says the format would need
|
|
1806
|
+
* conversion. Reading every attached .png off disk just to confirm it is
|
|
1807
|
+
* already compatible would double the I/O of the common case for no benefit.
|
|
1808
|
+
*/
|
|
1809
|
+
async function readImageSourceForConversion(payload) {
|
|
1810
|
+
if (Buffer.isBuffer(payload)) {
|
|
1811
|
+
const mimeType = detectMimeTypeFromBuffer(payload);
|
|
1812
|
+
return mimeType ? { buffer: payload, mimeType } : undefined;
|
|
1813
|
+
}
|
|
1814
|
+
if (typeof payload !== "string") {
|
|
1815
|
+
return undefined;
|
|
1816
|
+
}
|
|
1817
|
+
if (payload.startsWith("data:")) {
|
|
1818
|
+
const match = payload.match(/^data:([^;]+);base64,(.+)$/);
|
|
1819
|
+
return match
|
|
1820
|
+
? { buffer: Buffer.from(match[2], "base64"), mimeType: match[1] }
|
|
1821
|
+
: undefined;
|
|
1822
|
+
}
|
|
1823
|
+
if (isInternetUrl(payload)) {
|
|
1824
|
+
return undefined;
|
|
1825
|
+
}
|
|
1826
|
+
const mimeType = getMimeTypeFromExtension(payload);
|
|
1827
|
+
if (!needsVisionTranscode(mimeType)) {
|
|
1828
|
+
return undefined;
|
|
1829
|
+
}
|
|
1830
|
+
try {
|
|
1831
|
+
// Preflight the size before reading. Conversion replaces the entry with a
|
|
1832
|
+
// data URI, which bypasses processImageToBase64's buffer-size guard — so
|
|
1833
|
+
// without this a large local HEIC/TIFF/BMP was read fully into memory and
|
|
1834
|
+
// then re-encoded, with no limit applied at either step.
|
|
1835
|
+
const { size } = await statAsync(payload);
|
|
1836
|
+
ImageProcessor.validateSize(size, `image at ${safeBasename(payload)}`);
|
|
1837
|
+
const buffer = await readFileAsync(payload);
|
|
1838
|
+
ImageProcessor.validateBufferSize(buffer, `image at ${safeBasename(payload)}`);
|
|
1839
|
+
return { buffer, mimeType };
|
|
1840
|
+
}
|
|
1841
|
+
catch (error) {
|
|
1842
|
+
// The path may be a signed URL or carry credentials in query params, so it
|
|
1843
|
+
// is redacted rather than interpolated verbatim (see logSanitize).
|
|
1844
|
+
logger.warn(`[messageBuilder] Could not read ${redactUrlForError(payload)} for image ` +
|
|
1845
|
+
`format conversion: ${error instanceof Error ? error.message : String(error)}`);
|
|
1846
|
+
return undefined;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1769
1849
|
/**
|
|
1770
1850
|
* Convert simple images format to Vercel AI SDK format with smart auto-detection
|
|
1771
1851
|
* - URLs: Downloaded and converted to base64 for Vercel AI SDK compatibility
|
|
@@ -2,20 +2,34 @@
|
|
|
2
2
|
* File Extension Constants
|
|
3
3
|
* Centralized file extension definitions organized by category for file processing
|
|
4
4
|
*
|
|
5
|
+
* The media/document/data/archive categories are derived from
|
|
6
|
+
* {@link FILE_TYPE_REGISTRY} rather than typed out here — they used to be a
|
|
7
|
+
* second hand-maintained list that drifted from both the registry-equivalent
|
|
8
|
+
* MIME map and the detector, so formats appeared "supported" in one place and
|
|
9
|
+
* were invisible in another. Source-code and text categories, which carry no
|
|
10
|
+
* modality, are still enumerated below.
|
|
11
|
+
*
|
|
5
12
|
* @module processors/config/fileTypes
|
|
6
13
|
*/
|
|
7
14
|
/**
|
|
8
|
-
* Image file extensions supported by the platform
|
|
15
|
+
* Image file extensions supported by the platform.
|
|
16
|
+
* Includes `.svg`, which is processed as sanitized markup rather than raster.
|
|
9
17
|
*/
|
|
10
|
-
export declare const IMAGE_EXTENSIONS: readonly [
|
|
18
|
+
export declare const IMAGE_EXTENSIONS: readonly string[];
|
|
11
19
|
/**
|
|
12
|
-
*
|
|
20
|
+
* Image extensions every vision-capable provider accepts as-is.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately NOT derived from the registry: this is the intersection of what
|
|
23
|
+
* OpenAI, Anthropic, Google and Bedrock all accept, not the set NeuroLink can
|
|
24
|
+
* identify. Anything else the registry knows about (BMP, TIFF, AVIF, ICO,
|
|
25
|
+
* HEIC/HEIF) is transcoded to PNG before dispatch — see
|
|
26
|
+
* `adapters/imageFormatSupport.ts`.
|
|
13
27
|
*/
|
|
14
28
|
export declare const AI_VISION_EXTENSIONS: readonly [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
|
15
29
|
/**
|
|
16
30
|
* Document file extensions for office documents and PDFs
|
|
17
31
|
*/
|
|
18
|
-
export declare const DOCUMENT_EXTENSIONS: readonly [
|
|
32
|
+
export declare const DOCUMENT_EXTENSIONS: readonly string[];
|
|
19
33
|
/**
|
|
20
34
|
* PDF document extensions
|
|
21
35
|
*/
|
|
@@ -57,9 +71,12 @@ export declare const CSV_EXTENSIONS: readonly [".csv"];
|
|
|
57
71
|
*/
|
|
58
72
|
export declare const YAML_EXTENSIONS: readonly [".yaml", ".yml"];
|
|
59
73
|
/**
|
|
60
|
-
* All data format extensions combined
|
|
74
|
+
* All data format extensions combined.
|
|
75
|
+
*
|
|
76
|
+
* The tabular formats (.csv/.tsv) come from the registry; .json/.xml/.yaml are
|
|
77
|
+
* text formats with no modality of their own and stay listed here.
|
|
61
78
|
*/
|
|
62
|
-
export declare const DATA_EXTENSIONS: readonly [
|
|
79
|
+
export declare const DATA_EXTENSIONS: readonly string[];
|
|
63
80
|
/**
|
|
64
81
|
* Plain text file extensions
|
|
65
82
|
*/
|
|
@@ -173,15 +190,15 @@ export declare const CONFIG_EXTENSIONS: readonly [".env", ".ini", ".toml", ".cfg
|
|
|
173
190
|
/**
|
|
174
191
|
* Archive and compressed file extensions
|
|
175
192
|
*/
|
|
176
|
-
export declare const ARCHIVE_EXTENSIONS: readonly [
|
|
193
|
+
export declare const ARCHIVE_EXTENSIONS: readonly string[];
|
|
177
194
|
/**
|
|
178
195
|
* Video file extensions
|
|
179
196
|
*/
|
|
180
|
-
export declare const VIDEO_EXTENSIONS: readonly [
|
|
197
|
+
export declare const VIDEO_EXTENSIONS: readonly string[];
|
|
181
198
|
/**
|
|
182
199
|
* Audio file extensions
|
|
183
200
|
*/
|
|
184
|
-
export declare const AUDIO_EXTENSIONS: readonly [
|
|
201
|
+
export declare const AUDIO_EXTENSIONS: readonly string[];
|
|
185
202
|
/**
|
|
186
203
|
* Design and graphics file extensions
|
|
187
204
|
*/
|
|
@@ -198,16 +215,16 @@ export declare const EXECUTABLE_EXTENSIONS: readonly [".exe", ".dll", ".so", ".d
|
|
|
198
215
|
* File extensions grouped by category for easy access
|
|
199
216
|
*/
|
|
200
217
|
export declare const FILE_EXTENSIONS: {
|
|
201
|
-
readonly IMAGES: readonly [
|
|
218
|
+
readonly IMAGES: readonly string[];
|
|
202
219
|
readonly AI_VISION: readonly [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
|
203
|
-
readonly DOCUMENTS: readonly [
|
|
220
|
+
readonly DOCUMENTS: readonly string[];
|
|
204
221
|
readonly PDF: readonly [".pdf"];
|
|
205
222
|
readonly WORD: readonly [".docx", ".doc"];
|
|
206
223
|
readonly EXCEL: readonly [".xlsx", ".xls"];
|
|
207
224
|
readonly POWERPOINT: readonly [".pptx", ".ppt"];
|
|
208
225
|
readonly OPENDOCUMENT: readonly [".odt", ".ods", ".odp"];
|
|
209
226
|
readonly RTF: readonly [".rtf"];
|
|
210
|
-
readonly DATA: readonly [
|
|
227
|
+
readonly DATA: readonly string[];
|
|
211
228
|
readonly JSON: readonly [".json"];
|
|
212
229
|
readonly XML: readonly [".xml"];
|
|
213
230
|
readonly CSV: readonly [".csv"];
|
|
@@ -254,9 +271,9 @@ export declare const FILE_EXTENSIONS: {
|
|
|
254
271
|
};
|
|
255
272
|
readonly ALL_CODE: readonly [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".pyw", ".pyi", ".java", ".kt", ".kts", ".go", ".rs", ".c", ".h", ".cpp", ".hpp", ".cc", ".cxx", ".hxx", ".cs", ".rb", ".rake", ".php", ".phtml", ".php3", ".php4", ".php5", ".phps", ".sh", ".bash", ".zsh", ".fish", ".ksh", ".pl", ".pm", ".pod", ".t", ".lua", ".sql", ".swift", ".dart", ".m", ".mm", ".scala", ".sc", ".hs", ".lhs", ".ex", ".exs", ".erl", ".hrl", ".clj", ".cljs", ".cljc", ".edn", ".fs", ".fsx", ".fsi", ".ml", ".mli", ".lisp", ".lsp", ".cl", ".scm", ".ss", ".groovy", ".gvy", ".gy", ".gsh", ".ps1", ".psm1", ".psd1", ".r", ".rmd", ".jl", ".nim", ".nims", ".zig", ".v", ".cr", ".d", ".asm", ".s", ".f", ".f90", ".f95", ".f03", ".for", ".cob", ".cbl", ".cobol", ".pas", ".pp", ".p", ".ada", ".adb", ".ads", ".vue", ".svelte", ".hbs", ".handlebars", ".ejs", ".pug", ".jade", ".css", ".scss", ".sass", ".less", ".styl", ".stylus", ".dockerfile", ".mk"];
|
|
256
273
|
readonly CONFIG: readonly [".env", ".ini", ".toml", ".cfg", ".conf", ".config", ".properties", ".editorconfig", ".gitignore", ".gitattributes", ".npmrc", ".yarnrc", ".prettierrc", ".eslintrc", ".babelrc"];
|
|
257
|
-
readonly ARCHIVES: readonly [
|
|
258
|
-
readonly VIDEO: readonly [
|
|
259
|
-
readonly AUDIO: readonly [
|
|
274
|
+
readonly ARCHIVES: readonly string[];
|
|
275
|
+
readonly VIDEO: readonly string[];
|
|
276
|
+
readonly AUDIO: readonly string[];
|
|
260
277
|
readonly DESIGN: readonly [".psd", ".psb", ".ai", ".sketch", ".fig", ".xd"];
|
|
261
278
|
readonly DATABASE: readonly [".db", ".sqlite", ".sqlite3", ".mdb", ".accdb"];
|
|
262
279
|
readonly EXECUTABLE: readonly [".exe", ".dll", ".so", ".dylib", ".app", ".bat", ".cmd", ".vbs", ".ps1", ".msi", ".dmg", ".bin"];
|