@juspay/neurolink 10.11.2 → 10.12.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 +12 -0
- package/dist/adapters/audioFormatSupport.d.ts +53 -0
- package/dist/adapters/audioFormatSupport.js +200 -0
- package/dist/browser/neurolink.min.js +399 -398
- package/dist/cli/commands/auth.d.ts +8 -1
- package/dist/cli/commands/auth.js +185 -6
- package/dist/cli/factories/authCommandFactory.js +7 -1
- package/dist/lib/adapters/audioFormatSupport.d.ts +53 -0
- package/dist/lib/adapters/audioFormatSupport.js +201 -0
- package/dist/lib/processors/archive/ArchiveProcessor.d.ts +37 -0
- package/dist/lib/processors/archive/ArchiveProcessor.js +347 -32
- package/dist/lib/providers/googleAiStudio/client.d.ts +17 -0
- package/dist/lib/providers/googleAiStudio/client.js +45 -19
- package/dist/lib/providers/googleNativeGemini3/utils.d.ts +22 -1
- package/dist/lib/providers/googleNativeGemini3/utils.js +54 -0
- package/dist/lib/providers/googleVertex/client.js +3 -0
- package/dist/lib/proxy/accountQuota.d.ts +6 -0
- package/dist/lib/proxy/accountQuota.js +19 -2
- package/dist/lib/proxy/accountUsage.d.ts +45 -0
- package/dist/lib/proxy/accountUsage.js +289 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +15 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +166 -0
- package/dist/lib/types/cli.d.ts +12 -0
- package/dist/lib/types/file.d.ts +41 -0
- package/dist/lib/types/generate.d.ts +12 -1
- package/dist/lib/types/processor.d.ts +20 -1
- package/dist/lib/types/providers.d.ts +7 -0
- package/dist/lib/types/proxy.d.ts +101 -0
- package/dist/lib/utils/fileDetector.d.ts +27 -0
- package/dist/lib/utils/fileDetector.js +130 -7
- package/dist/lib/utils/imageProcessor.js +31 -0
- package/dist/lib/utils/messageBuilder.d.ts +0 -9
- package/dist/lib/utils/messageBuilder.js +380 -56
- package/dist/processors/archive/ArchiveProcessor.d.ts +37 -0
- package/dist/processors/archive/ArchiveProcessor.js +347 -32
- package/dist/providers/googleAiStudio/client.d.ts +17 -0
- package/dist/providers/googleAiStudio/client.js +45 -19
- package/dist/providers/googleNativeGemini3/utils.d.ts +22 -1
- package/dist/providers/googleNativeGemini3/utils.js +54 -0
- package/dist/providers/googleVertex/client.js +3 -0
- package/dist/proxy/accountQuota.d.ts +6 -0
- package/dist/proxy/accountQuota.js +19 -2
- package/dist/proxy/accountUsage.d.ts +45 -0
- package/dist/proxy/accountUsage.js +288 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +15 -1
- package/dist/server/routes/claudeProxyRoutes.js +166 -0
- package/dist/types/cli.d.ts +12 -0
- package/dist/types/file.d.ts +41 -0
- package/dist/types/generate.d.ts +12 -1
- package/dist/types/processor.d.ts +20 -1
- package/dist/types/providers.d.ts +7 -0
- package/dist/types/proxy.d.ts +101 -0
- package/dist/utils/fileDetector.d.ts +27 -0
- package/dist/utils/fileDetector.js +130 -7
- package/dist/utils/imageProcessor.js +31 -0
- package/dist/utils/messageBuilder.d.ts +0 -9
- package/dist/utils/messageBuilder.js +380 -56
- package/package.json +3 -2
|
@@ -143,10 +143,27 @@ const SUPPORTED_ARCHIVE_MIME_TYPES = [
|
|
|
143
143
|
"application/x-gzip",
|
|
144
144
|
"application/x-compressed-tar",
|
|
145
145
|
"application/x-bzip2",
|
|
146
|
+
// XZ and Zstandard, added alongside the extensions above: a caller that
|
|
147
|
+
// uploads bytes with a declared mimetype and no filename was rejected at the
|
|
148
|
+
// type gate for the two formats this change teaches the processor to read.
|
|
149
|
+
"application/x-xz",
|
|
150
|
+
"application/zstd",
|
|
146
151
|
"application/java-archive",
|
|
147
152
|
];
|
|
153
|
+
/**
|
|
154
|
+
* External decompressor per single-stream format.
|
|
155
|
+
*
|
|
156
|
+
* One map rather than a ternary repeated at each use: the decompressor and the
|
|
157
|
+
* error message that names it were derived separately, so they could disagree
|
|
158
|
+
* about which binary the user is being told to install.
|
|
159
|
+
*/
|
|
160
|
+
const SINGLE_STREAM_TOOLS = {
|
|
161
|
+
bz2: "bzip2",
|
|
162
|
+
xz: "xz",
|
|
163
|
+
zst: "zstd",
|
|
164
|
+
};
|
|
148
165
|
/** File extensions recognized as archive formats */
|
|
149
|
-
const SUPPORTED_ARCHIVE_EXTENSIONS = [".zip", ".tar", ".gz", ".tgz", ".bz2", ".tbz2", ".jar"];
|
|
166
|
+
const SUPPORTED_ARCHIVE_EXTENSIONS = [".zip", ".tar", ".gz", ".tgz", ".bz2", ".tbz2", ".jar", ".xz", ".txz", ".zst", ".tzst"];
|
|
150
167
|
// =============================================================================
|
|
151
168
|
// MAGIC BYTE SIGNATURES
|
|
152
169
|
// =============================================================================
|
|
@@ -163,8 +180,20 @@ const MAGIC_BYTES = {
|
|
|
163
180
|
ZIP_SPANNED: [0x50, 0x4b, 0x07, 0x08],
|
|
164
181
|
/** GZIP: \x1f\x8b */
|
|
165
182
|
GZIP: [0x1f, 0x8b],
|
|
166
|
-
/**
|
|
167
|
-
|
|
183
|
+
/**
|
|
184
|
+
* BZIP2: `BZh` — all three bytes, not just `BZ`.
|
|
185
|
+
*
|
|
186
|
+
* Two bytes was harmless while every BZIP2 match was rejected outright as an
|
|
187
|
+
* unsupported format. Now that a match actually spawns `bzip2`, any buffer
|
|
188
|
+
* beginning with the ASCII letters "BZ" is handed to the decompressor and
|
|
189
|
+
* comes back reported as a corrupt BZ2 stream — a confident wrong answer
|
|
190
|
+
* about a file that was never bzip2 at all.
|
|
191
|
+
*/
|
|
192
|
+
BZIP2: [0x42, 0x5a, 0x68],
|
|
193
|
+
/** XZ: \xfd7zXZ\x00 */
|
|
194
|
+
XZ: [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00],
|
|
195
|
+
/** Zstandard frame: \x28\xb5\x2f\xfd */
|
|
196
|
+
ZSTD: [0x28, 0xb5, 0x2f, 0xfd],
|
|
168
197
|
/** RAR: Rar!\x1a\x07 */
|
|
169
198
|
RAR: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07],
|
|
170
199
|
/** 7-Zip: 7z\xbc\xaf\x27\x1c */
|
|
@@ -315,7 +344,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
315
344
|
error: this.createError(FileErrorCode.UNSUPPORTED_TYPE, {
|
|
316
345
|
format,
|
|
317
346
|
reason: `${format.toUpperCase()} archives are not yet supported. Please convert to ZIP or TAR format.`,
|
|
318
|
-
supportedFormats: "ZIP, TAR, TAR.GZ, GZ",
|
|
347
|
+
supportedFormats: "ZIP, TAR, TAR.GZ, TAR.BZ2, GZ, BZ2, XZ, ZST, JAR",
|
|
319
348
|
}),
|
|
320
349
|
};
|
|
321
350
|
}
|
|
@@ -356,7 +385,9 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
356
385
|
// Step 9: Extract content from text-based entries (Phase 2 sub-processing)
|
|
357
386
|
// For ZIP archives, extract and include content from small text-based files.
|
|
358
387
|
// Skips nested archives and binary files for safety.
|
|
359
|
-
|
|
388
|
+
// ZIP re-parses to pull member text; every other format collects it while
|
|
389
|
+
// decompressing, because their bytes stream past exactly once.
|
|
390
|
+
let extractedContents = extractionResult.contents ?? new Map();
|
|
360
391
|
if (format === "zip") {
|
|
361
392
|
extractedContents = await this.extractEntryContents(buffer, entries);
|
|
362
393
|
}
|
|
@@ -417,6 +448,18 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
417
448
|
// Could still be a tar.gz without the extension - we'll detect during extraction
|
|
418
449
|
return "gz";
|
|
419
450
|
}
|
|
451
|
+
// Bzip2 wraps a tar just as often, and now that a BZIP2 magic match
|
|
452
|
+
// resolves to a real format rather than being rejected outright, the
|
|
453
|
+
// same filename check has to run here too. Without it every genuine
|
|
454
|
+
// `.tar.bz2` reported its format as "BZ2" to the model — extraction was
|
|
455
|
+
// right, the label was not — and the `.tar.bz2` extension mapping below
|
|
456
|
+
// became unreachable for any well-formed file.
|
|
457
|
+
if (magicFormat === "bz2") {
|
|
458
|
+
const ext = filename.toLowerCase();
|
|
459
|
+
return ext.endsWith(".tar.bz2") || ext.endsWith(".tbz2")
|
|
460
|
+
? "tar.bz2"
|
|
461
|
+
: "bz2";
|
|
462
|
+
}
|
|
420
463
|
return magicFormat;
|
|
421
464
|
}
|
|
422
465
|
// Fallback to extension-based detection
|
|
@@ -451,9 +494,19 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
451
494
|
if (this.matchesMagic(buffer, MAGIC_BYTES.GZIP)) {
|
|
452
495
|
return "gz";
|
|
453
496
|
}
|
|
454
|
-
// Check for BZIP2 (
|
|
497
|
+
// Check for BZIP2 (3 bytes: "BZh"). A bare .bz2 is a single compressed
|
|
498
|
+
// file; the caller decides whether the decompressed bytes turn out to be a
|
|
499
|
+
// tar.
|
|
455
500
|
if (this.matchesMagic(buffer, MAGIC_BYTES.BZIP2)) {
|
|
456
|
-
return "
|
|
501
|
+
return "bz2";
|
|
502
|
+
}
|
|
503
|
+
// Check for XZ (6 bytes)
|
|
504
|
+
if (buffer.length >= 6 && this.matchesMagic(buffer, MAGIC_BYTES.XZ)) {
|
|
505
|
+
return "xz";
|
|
506
|
+
}
|
|
507
|
+
// Check for Zstandard (4 bytes)
|
|
508
|
+
if (buffer.length >= 4 && this.matchesMagic(buffer, MAGIC_BYTES.ZSTD)) {
|
|
509
|
+
return "zst";
|
|
457
510
|
}
|
|
458
511
|
return null;
|
|
459
512
|
}
|
|
@@ -478,7 +531,21 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
478
531
|
return "gz";
|
|
479
532
|
}
|
|
480
533
|
if (lowerFilename.endsWith(".bz2")) {
|
|
481
|
-
|
|
534
|
+
// A bare `.bz2` is a single compressed file, not a tarball. Reporting it
|
|
535
|
+
// as "tar.bz2" sent it down a branch that refused the format outright.
|
|
536
|
+
return "bz2";
|
|
537
|
+
}
|
|
538
|
+
if (lowerFilename.endsWith(".tar.xz") || lowerFilename.endsWith(".txz")) {
|
|
539
|
+
return "xz";
|
|
540
|
+
}
|
|
541
|
+
if (lowerFilename.endsWith(".xz")) {
|
|
542
|
+
return "xz";
|
|
543
|
+
}
|
|
544
|
+
if (lowerFilename.endsWith(".tar.zst") || lowerFilename.endsWith(".tzst")) {
|
|
545
|
+
return "zst";
|
|
546
|
+
}
|
|
547
|
+
if (lowerFilename.endsWith(".zst")) {
|
|
548
|
+
return "zst";
|
|
482
549
|
}
|
|
483
550
|
if (lowerFilename.endsWith(".zip") || lowerFilename.endsWith(".jar")) {
|
|
484
551
|
return "zip";
|
|
@@ -526,16 +593,12 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
526
593
|
case "tar.gz":
|
|
527
594
|
return this.extractTarGzEntries(buffer);
|
|
528
595
|
case "tar.bz2":
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
reason: "TAR.BZ2 archives are not yet supported. Please convert to ZIP or TAR.GZ format.",
|
|
536
|
-
supportedFormats: "ZIP, TAR, TAR.GZ, GZ",
|
|
537
|
-
}),
|
|
538
|
-
};
|
|
596
|
+
case "bz2":
|
|
597
|
+
return this.extractSingleStreamEntries(buffer, "bz2");
|
|
598
|
+
case "xz":
|
|
599
|
+
return this.extractSingleStreamEntries(buffer, "xz");
|
|
600
|
+
case "zst":
|
|
601
|
+
return this.extractSingleStreamEntries(buffer, "zst");
|
|
539
602
|
case "gz":
|
|
540
603
|
return this.extractGzEntries(buffer);
|
|
541
604
|
default:
|
|
@@ -546,7 +609,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
546
609
|
error: this.createError(FileErrorCode.UNSUPPORTED_TYPE, {
|
|
547
610
|
format,
|
|
548
611
|
reason: `${format} archives are not supported`,
|
|
549
|
-
supportedFormats: "ZIP, TAR, TAR.GZ, GZ",
|
|
612
|
+
supportedFormats: "ZIP, TAR, TAR.GZ, TAR.BZ2, GZ, BZ2, XZ, ZST, JAR",
|
|
550
613
|
}),
|
|
551
614
|
};
|
|
552
615
|
}
|
|
@@ -767,8 +830,10 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
767
830
|
return new Promise((resolve) => {
|
|
768
831
|
const entries = [];
|
|
769
832
|
const securityWarnings = [];
|
|
833
|
+
const contents = new Map();
|
|
770
834
|
let entryCount = 0;
|
|
771
835
|
let cumulativeSize = 0;
|
|
836
|
+
let extractedBytes = 0;
|
|
772
837
|
let earlyError = null;
|
|
773
838
|
const extract = tarStream.extract();
|
|
774
839
|
extract.on("entry", (header, stream, next) => {
|
|
@@ -827,6 +892,33 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
827
892
|
compressedSize: 0, // TAR doesn't compress individual entries
|
|
828
893
|
isDirectory,
|
|
829
894
|
});
|
|
895
|
+
// Capture the text of eligible members as they go past.
|
|
896
|
+
//
|
|
897
|
+
// A listing alone is not an answer. Asked what a .tar contains, the
|
|
898
|
+
// model could previously only recite filenames and sizes — the same
|
|
899
|
+
// archive as a .zip returned its text, because only the ZIP path
|
|
900
|
+
// extracted anything. A tar entry's bytes are available exactly once,
|
|
901
|
+
// here, while the stream is open; re-reading later would mean parsing
|
|
902
|
+
// the whole archive a second time.
|
|
903
|
+
if (!isDirectory &&
|
|
904
|
+
this.isExtractableEntryName(entryName) &&
|
|
905
|
+
entrySize > 0 &&
|
|
906
|
+
entrySize <= ARCHIVE_CONFIG.MAX_EXTRACT_ENTRY_SIZE &&
|
|
907
|
+
contents.size < ARCHIVE_CONFIG.MAX_EXTRACT_ENTRIES &&
|
|
908
|
+
extractedBytes + entrySize <= ARCHIVE_CONFIG.MAX_TOTAL_EXTRACT_SIZE) {
|
|
909
|
+
const chunks = [];
|
|
910
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
911
|
+
stream.on("end", () => {
|
|
912
|
+
const text = this.decodeEntryText(Buffer.concat(chunks));
|
|
913
|
+
if (text !== null) {
|
|
914
|
+
contents.set(entryName, text);
|
|
915
|
+
extractedBytes += entrySize;
|
|
916
|
+
}
|
|
917
|
+
next();
|
|
918
|
+
});
|
|
919
|
+
stream.on("error", () => next());
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
830
922
|
// Consume the stream without buffering (we only need metadata)
|
|
831
923
|
stream.resume();
|
|
832
924
|
next();
|
|
@@ -841,7 +933,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
841
933
|
});
|
|
842
934
|
}
|
|
843
935
|
else {
|
|
844
|
-
resolve({ success: true, entries, securityWarnings });
|
|
936
|
+
resolve({ success: true, entries, securityWarnings, contents });
|
|
845
937
|
}
|
|
846
938
|
});
|
|
847
939
|
extract.on("error", (err) => {
|
|
@@ -929,7 +1021,14 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
929
1021
|
isDirectory: false,
|
|
930
1022
|
},
|
|
931
1023
|
];
|
|
932
|
-
|
|
1024
|
+
// The decompressed bytes ARE the content here; a listing that says
|
|
1025
|
+
// "decompressed-content (1.02 MB)" answers nothing about the file.
|
|
1026
|
+
const contents = new Map();
|
|
1027
|
+
const gzText = this.decodeEntryText(decompressed);
|
|
1028
|
+
if (gzText !== null) {
|
|
1029
|
+
contents.set(innerFilename, gzText.slice(0, ARCHIVE_CONFIG.MAX_EXTRACT_ENTRY_SIZE));
|
|
1030
|
+
}
|
|
1031
|
+
return { success: true, entries, securityWarnings, contents };
|
|
933
1032
|
}
|
|
934
1033
|
catch (error) {
|
|
935
1034
|
return {
|
|
@@ -999,6 +1098,232 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
999
1098
|
* @param entries - Previously extracted entry metadata
|
|
1000
1099
|
* @returns Map of entry name to extracted text content
|
|
1001
1100
|
*/
|
|
1101
|
+
/**
|
|
1102
|
+
* Whether an entry name looks like something worth inlining as text.
|
|
1103
|
+
*
|
|
1104
|
+
* Shared by the ZIP and TAR paths so the two cannot drift into disagreeing
|
|
1105
|
+
* about which members are worth reading — they did, because only ZIP had the
|
|
1106
|
+
* rule at all.
|
|
1107
|
+
*/
|
|
1108
|
+
isExtractableEntryName(name) {
|
|
1109
|
+
const ext = path.extname(name).toLowerCase();
|
|
1110
|
+
if (ARCHIVE_CONFIG.EXTRACTABLE_EXTENSIONS.has(ext)) {
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
const base = path.basename(name).toLowerCase();
|
|
1114
|
+
return (base === "readme" ||
|
|
1115
|
+
base === "license" ||
|
|
1116
|
+
base === "makefile" ||
|
|
1117
|
+
base === "dockerfile");
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Decode bytes to text, or null when they are not text.
|
|
1121
|
+
*
|
|
1122
|
+
* A NUL byte in the first 512 bytes, or a high proportion of replacement
|
|
1123
|
+
* characters after decoding, means binary — inlining that would spend the
|
|
1124
|
+
* extraction budget on mojibake.
|
|
1125
|
+
*/
|
|
1126
|
+
decodeEntryText(data) {
|
|
1127
|
+
if (!data || data.length === 0) {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
if (data.subarray(0, Math.min(512, data.length)).includes(0)) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
const text = data.toString("utf-8");
|
|
1134
|
+
const replacements = (text.match(/\ufffd/g) || []).length;
|
|
1135
|
+
return replacements > text.length * 0.05 ? null : text;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Decompress a single-stream archive (.bz2, .xz, .zst).
|
|
1139
|
+
*
|
|
1140
|
+
* Node ships zstd from v22.15/23, so that one needs no help. bzip2 and xz
|
|
1141
|
+
* have no Node binding, and adding a native module for them would make an
|
|
1142
|
+
* optional format a build-time dependency for every consumer — so the system
|
|
1143
|
+
* tools are used when present, the same soft-dependency arrangement this
|
|
1144
|
+
* codebase already has with ffmpeg. Absent tooling returns null and the
|
|
1145
|
+
* caller reports the format as unsupported *on this machine* rather than
|
|
1146
|
+
* unsupported in principle.
|
|
1147
|
+
*/
|
|
1148
|
+
async decompressSingleStream(buffer, format) {
|
|
1149
|
+
if (format === "zst") {
|
|
1150
|
+
// Node gained zstd in v22.15/23. Older runtimes simply lack the export,
|
|
1151
|
+
// so it is probed at runtime rather than assumed from the typings —
|
|
1152
|
+
// which is also why this is a property check on an `unknown` module
|
|
1153
|
+
// instead of a cast asserting it exists.
|
|
1154
|
+
const zlibModule = await import("zlib");
|
|
1155
|
+
const candidate = typeof zlibModule === "object" && zlibModule !== null
|
|
1156
|
+
? zlibModule.zstdDecompress
|
|
1157
|
+
: undefined;
|
|
1158
|
+
if (typeof candidate === "function") {
|
|
1159
|
+
const zstdDecompress = candidate;
|
|
1160
|
+
return await new Promise((resolve) => {
|
|
1161
|
+
// Bounded at the decoder rather than after the fact. The size guard
|
|
1162
|
+
// downstream only runs once the whole buffer exists, which is too
|
|
1163
|
+
// late for a zip-bomb: a few KB of zstd expands to gigabytes and the
|
|
1164
|
+
// allocation is what hurts. `maxOutputLength` makes it fail fast.
|
|
1165
|
+
zstdDecompress(buffer, { maxOutputLength: ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE }, (err, res) => resolve(err
|
|
1166
|
+
? {
|
|
1167
|
+
status: err.code ===
|
|
1168
|
+
"ERR_BUFFER_TOO_LARGE"
|
|
1169
|
+
? "too-large"
|
|
1170
|
+
: "failed",
|
|
1171
|
+
}
|
|
1172
|
+
: { status: "ok", buffer: res }));
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const tool = SINGLE_STREAM_TOOLS[format];
|
|
1177
|
+
try {
|
|
1178
|
+
const { execFile } = await import("node:child_process");
|
|
1179
|
+
return await new Promise((resolve) => {
|
|
1180
|
+
// ENOENT means the binary is absent; anything else means it ran and
|
|
1181
|
+
// could not do the job. Only the former justifies telling the caller to
|
|
1182
|
+
// install something.
|
|
1183
|
+
//
|
|
1184
|
+
// Read off the error itself rather than a flag set by the `error`
|
|
1185
|
+
// listener below, because execFile invokes this callback BEFORE that
|
|
1186
|
+
// listener runs — measured, not assumed:
|
|
1187
|
+
// callback(missing=false, code=ENOENT) → errorListener(code=ENOENT)
|
|
1188
|
+
// The promise is already settled by the time the listener could set the
|
|
1189
|
+
// flag, so a missing decompressor reported itself as a corrupt stream
|
|
1190
|
+
// and told the user to re-upload a perfectly good file.
|
|
1191
|
+
const isMissingTool = (error) => error?.code === "ENOENT";
|
|
1192
|
+
// Hitting `maxBuffer` is the zip-bomb guard firing, not a corrupt
|
|
1193
|
+
// stream: it will fail identically on every retry, so it must not be
|
|
1194
|
+
// reported as retryable the way a truncated upload is.
|
|
1195
|
+
const isTooLarge = (error) => error?.code ===
|
|
1196
|
+
"ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
1197
|
+
const child = execFile(tool, ["-dc"], {
|
|
1198
|
+
encoding: "buffer",
|
|
1199
|
+
maxBuffer: ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE,
|
|
1200
|
+
// Without this a crafted or truncated stream that makes the tool
|
|
1201
|
+
// block forever never fires the callback, so the promise never
|
|
1202
|
+
// settles and the request that awaits it hangs indefinitely.
|
|
1203
|
+
timeout: ARCHIVE_CONFIG.TIMEOUT_MS,
|
|
1204
|
+
}, (error, stdout) => {
|
|
1205
|
+
// Only a non-zero exit means failure. Empty output does not:
|
|
1206
|
+
// compressing an empty file is legal and round-trips — a 32-byte
|
|
1207
|
+
// `empty.txt.xz` decompresses to nothing with exit 0 — so treating
|
|
1208
|
+
// zero length as an error reported a valid archive as corrupt. The
|
|
1209
|
+
// in-process zstd path above already keyed on the error alone, so
|
|
1210
|
+
// the two backends disagreed about the same input.
|
|
1211
|
+
if (error || !stdout) {
|
|
1212
|
+
resolve({
|
|
1213
|
+
status: isMissingTool(error)
|
|
1214
|
+
? "tool-unavailable"
|
|
1215
|
+
: isTooLarge(error)
|
|
1216
|
+
? "too-large"
|
|
1217
|
+
: "failed",
|
|
1218
|
+
});
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
resolve({ status: "ok", buffer: Buffer.from(stdout) });
|
|
1222
|
+
});
|
|
1223
|
+
// Kept as a backstop for spawn failures that never reach the
|
|
1224
|
+
// callback. Whichever settles first wins; both now classify the same
|
|
1225
|
+
// way, so the outcome no longer depends on the order.
|
|
1226
|
+
child.on("error", (error) => {
|
|
1227
|
+
resolve({
|
|
1228
|
+
status: isMissingTool(error) ? "tool-unavailable" : "failed",
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
// Swallowed, deliberately not resolved from. A stdin write fails with
|
|
1232
|
+
// EPIPE precisely because the child already died — including when Node
|
|
1233
|
+
// killed it for exceeding `maxBuffer`, which is the zip-bomb guard.
|
|
1234
|
+
// Resolving here raced the exec callback and, when it won, downgraded a
|
|
1235
|
+
// "too-large" verdict to a retryable "failed", telling the caller to
|
|
1236
|
+
// retry a bomb. The callback is the authoritative signal: it always
|
|
1237
|
+
// fires once the process exits or fails to spawn, and the `timeout`
|
|
1238
|
+
// above bounds the wait. This listener exists only so an unhandled
|
|
1239
|
+
// 'error' event cannot take the process down.
|
|
1240
|
+
child.stdin?.on("error", () => undefined);
|
|
1241
|
+
child.stdin?.end(buffer);
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
return { status: "tool-unavailable" };
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Extract a single-stream archive: decompress, then treat the result as a
|
|
1250
|
+
* TAR when it is one and as a lone file otherwise.
|
|
1251
|
+
*
|
|
1252
|
+
* The tar check matters because `.tar.xz` and `.tar.zst` are how these
|
|
1253
|
+
* formats are usually met — reporting one opaque "decompressed-content" blob
|
|
1254
|
+
* for an archive of forty files would be technically true and useless.
|
|
1255
|
+
*/
|
|
1256
|
+
async extractSingleStreamEntries(buffer, format) {
|
|
1257
|
+
const result = await this.decompressSingleStream(buffer, format);
|
|
1258
|
+
if (result.status !== "ok") {
|
|
1259
|
+
const tool = SINGLE_STREAM_TOOLS[format];
|
|
1260
|
+
// Three distinct outcomes, and the codes carry contracts a caller acts
|
|
1261
|
+
// on. UNSUPPORTED_TYPE is `retryable: false` and advises converting the
|
|
1262
|
+
// file; DECOMPRESSION_FAILED is `retryable: true` and advises
|
|
1263
|
+
// re-uploading; SECURITY_VALIDATION_FAILED is how the GZIP and TAR.GZ
|
|
1264
|
+
// paths in this file already report a decompression bomb. `retryable` is
|
|
1265
|
+
// read downstream by `isRetryableErrorCode`, so a stream that tripped the
|
|
1266
|
+
// decoder's size cap must not be advertised as worth retrying — it will
|
|
1267
|
+
// fail identically every time — and a merely truncated upload must not be
|
|
1268
|
+
// reported as an unsupported format.
|
|
1269
|
+
const code = result.status === "tool-unavailable"
|
|
1270
|
+
? FileErrorCode.UNSUPPORTED_TYPE
|
|
1271
|
+
: result.status === "too-large"
|
|
1272
|
+
? FileErrorCode.SECURITY_VALIDATION_FAILED
|
|
1273
|
+
: FileErrorCode.DECOMPRESSION_FAILED;
|
|
1274
|
+
return {
|
|
1275
|
+
success: false,
|
|
1276
|
+
entries: [],
|
|
1277
|
+
securityWarnings: [],
|
|
1278
|
+
error: this.createError(code, {
|
|
1279
|
+
format,
|
|
1280
|
+
reason: result.status === "tool-unavailable"
|
|
1281
|
+
? `${format.toUpperCase()} could not be decompressed. Node has no built-in ` +
|
|
1282
|
+
`decoder for it and the "${tool}" command is unavailable on this machine.`
|
|
1283
|
+
: result.status === "too-large"
|
|
1284
|
+
? `${format.toUpperCase()} expands beyond the ` +
|
|
1285
|
+
`${this.formatSizeMB(ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE)} MB ` +
|
|
1286
|
+
`decompression limit and was refused before it could be read.`
|
|
1287
|
+
: `${format.toUpperCase()} could not be decompressed. The stream is ` +
|
|
1288
|
+
`corrupt or truncated.`,
|
|
1289
|
+
supportedFormats: "ZIP, TAR, TAR.GZ, TAR.BZ2, GZ, BZ2, XZ, ZST, JAR",
|
|
1290
|
+
}),
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
const decompressed = result.buffer;
|
|
1294
|
+
if (decompressed.length > ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE) {
|
|
1295
|
+
return {
|
|
1296
|
+
success: false,
|
|
1297
|
+
entries: [],
|
|
1298
|
+
securityWarnings: [],
|
|
1299
|
+
error: this.createError(FileErrorCode.SECURITY_VALIDATION_FAILED, {
|
|
1300
|
+
reason: `Decompressed size (${this.formatSizeMB(decompressed.length)} MB) exceeds limit (${this.formatSizeMB(ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE)} MB)`,
|
|
1301
|
+
}),
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
if (this.looksLikeTar(decompressed)) {
|
|
1305
|
+
const tarStream = await import("tar-stream");
|
|
1306
|
+
return await this.parseTarStream(tarStream, decompressed);
|
|
1307
|
+
}
|
|
1308
|
+
const contents = new Map();
|
|
1309
|
+
const text = this.decodeEntryText(decompressed);
|
|
1310
|
+
if (text !== null) {
|
|
1311
|
+
contents.set("decompressed-content", text.slice(0, ARCHIVE_CONFIG.MAX_EXTRACT_ENTRY_SIZE));
|
|
1312
|
+
}
|
|
1313
|
+
return {
|
|
1314
|
+
success: true,
|
|
1315
|
+
entries: [
|
|
1316
|
+
{
|
|
1317
|
+
name: "decompressed-content",
|
|
1318
|
+
uncompressedSize: decompressed.length,
|
|
1319
|
+
compressedSize: buffer.length,
|
|
1320
|
+
isDirectory: false,
|
|
1321
|
+
},
|
|
1322
|
+
],
|
|
1323
|
+
securityWarnings: [],
|
|
1324
|
+
contents,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1002
1327
|
async extractEntryContents(buffer, entries) {
|
|
1003
1328
|
const contents = new Map();
|
|
1004
1329
|
try {
|
|
@@ -1016,17 +1341,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
|
|
|
1016
1341
|
if (e.uncompressedSize === 0) {
|
|
1017
1342
|
return false;
|
|
1018
1343
|
}
|
|
1019
|
-
|
|
1020
|
-
// Check by extension
|
|
1021
|
-
if (ARCHIVE_CONFIG.EXTRACTABLE_EXTENSIONS.has(ext)) {
|
|
1022
|
-
return true;
|
|
1023
|
-
}
|
|
1024
|
-
// Check for common extensionless config files
|
|
1025
|
-
const basename = path.basename(e.name).toLowerCase();
|
|
1026
|
-
if (basename === "readme" || basename === "license" || basename === "makefile" || basename === "dockerfile") {
|
|
1027
|
-
return true;
|
|
1028
|
-
}
|
|
1029
|
-
return false;
|
|
1344
|
+
return this.isExtractableEntryName(e.name);
|
|
1030
1345
|
})
|
|
1031
1346
|
// Sort: smaller files first (more likely to fit), then by name
|
|
1032
1347
|
.sort((a, b) => a.uncompressedSize - b.uncompressedSize);
|
|
@@ -30,6 +30,23 @@ export declare class GoogleAIStudioProvider extends BaseProvider {
|
|
|
30
30
|
* Estimate token count from text using centralized estimation with provider multipliers
|
|
31
31
|
*/
|
|
32
32
|
private estimateTokenCount;
|
|
33
|
+
/**
|
|
34
|
+
* Run the file preprocessing this provider's native paths depend on.
|
|
35
|
+
*
|
|
36
|
+
* AI Studio overrides both `generate()` and `executeStream()` and routes
|
|
37
|
+
* straight to the native SDK, so neither reaches
|
|
38
|
+
* `buildMultimodalMessagesArray` — the place that turns `input.files` into
|
|
39
|
+
* text, images, PDFs and `nativeAudioFiles`. `BaseProvider.stream()` does
|
|
40
|
+
* build messages, but onto a throwaway clone whose result is discarded, so
|
|
41
|
+
* the real `options.input` came through untouched.
|
|
42
|
+
*
|
|
43
|
+
* The consequence was asymmetric and easy to miss: `generate()` did this
|
|
44
|
+
* inline and worked, while `stream()` silently dropped every attached file —
|
|
45
|
+
* not just audio, but the metadata summary too. Vertex hit the identical bug
|
|
46
|
+
* (#1258) and solved it with exactly this shape, called from both entry
|
|
47
|
+
* points.
|
|
48
|
+
*/
|
|
49
|
+
private preprocessNativeFileInput;
|
|
33
50
|
protected executeStream(options: StreamOptions, analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
|
|
34
51
|
/**
|
|
35
52
|
* Execute stream using native @google/genai SDK
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ErrorCategory, ErrorSeverity, GoogleAIModels, } from "../../constants/enums.js";
|
|
2
2
|
import { BaseProvider } from "../../core/baseProvider.js";
|
|
3
3
|
import { IMAGE_GENERATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../../core/constants.js";
|
|
4
|
-
import { normalizeVisionImageFormats, processUnifiedFilesArray, } from "../../utils/messageBuilder.js";
|
|
4
|
+
import { mergeMediaFileAliases, normalizeVisionImageFormats, processUnifiedFilesArray, } from "../../utils/messageBuilder.js";
|
|
5
5
|
import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../../telemetry/index.js";
|
|
6
6
|
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
|
|
7
7
|
import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
|
|
@@ -513,12 +513,55 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
513
513
|
return estimateTokens(text, "google-ai");
|
|
514
514
|
}
|
|
515
515
|
// executeGenerate removed - BaseProvider handles all generation with tools
|
|
516
|
+
/**
|
|
517
|
+
* Run the file preprocessing this provider's native paths depend on.
|
|
518
|
+
*
|
|
519
|
+
* AI Studio overrides both `generate()` and `executeStream()` and routes
|
|
520
|
+
* straight to the native SDK, so neither reaches
|
|
521
|
+
* `buildMultimodalMessagesArray` — the place that turns `input.files` into
|
|
522
|
+
* text, images, PDFs and `nativeAudioFiles`. `BaseProvider.stream()` does
|
|
523
|
+
* build messages, but onto a throwaway clone whose result is discarded, so
|
|
524
|
+
* the real `options.input` came through untouched.
|
|
525
|
+
*
|
|
526
|
+
* The consequence was asymmetric and easy to miss: `generate()` did this
|
|
527
|
+
* inline and worked, while `stream()` silently dropped every attached file —
|
|
528
|
+
* not just audio, but the metadata summary too. Vertex hit the identical bug
|
|
529
|
+
* (#1258) and solved it with exactly this shape, called from both entry
|
|
530
|
+
* points.
|
|
531
|
+
*/
|
|
532
|
+
async preprocessNativeFileInput(options) {
|
|
533
|
+
// The user-facing aliases (`input.audioFiles`, `input.videoFiles`) are
|
|
534
|
+
// folded into `input.files` here, exactly as the Vertex client does. Only
|
|
535
|
+
// `files` is processed below, so without this a caller who used the
|
|
536
|
+
// documented `audioFiles` field had it silently ignored on both of this
|
|
537
|
+
// provider's paths.
|
|
538
|
+
if (options.input) {
|
|
539
|
+
mergeMediaFileAliases(options.input);
|
|
540
|
+
}
|
|
541
|
+
if (options.input?.files && options.input.files.length > 0) {
|
|
542
|
+
try {
|
|
543
|
+
// Mutates options.input.text / .images / .pdfFiles / .nativeAudioFiles
|
|
544
|
+
// in place.
|
|
545
|
+
await processUnifiedFilesArray(options, 100 * 1024 * 1024, this.providerName);
|
|
546
|
+
}
|
|
547
|
+
catch (fileError) {
|
|
548
|
+
logger.warn(`[GoogleAIStudio] processUnifiedFilesArray threw, continuing without file content: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Runs even without input.files: a caller can populate input.images
|
|
552
|
+
// directly, and this native path never reaches the shared multimodal
|
|
553
|
+
// builder that would otherwise normalize the formats.
|
|
554
|
+
await normalizeVisionImageFormats(options.input);
|
|
555
|
+
}
|
|
516
556
|
async executeStream(options, analysisSchema) {
|
|
517
557
|
const modelName = options.model || this.modelName;
|
|
518
558
|
// Phase 1: if audio input present, bridge to Gemini Live (Studio) using @google/genai
|
|
519
559
|
if (options.input?.audio) {
|
|
520
560
|
return await this.executeAudioStreamViaGeminiLive(options);
|
|
521
561
|
}
|
|
562
|
+
// #1258, for this provider: stream() must run the same file preprocessing
|
|
563
|
+
// generate() does, or attached files are dropped on this path alone.
|
|
564
|
+
await this.preprocessNativeFileInput(options);
|
|
522
565
|
// Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
|
|
523
566
|
const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
|
|
524
567
|
// Tool filter (a0269210): trust options.tools — caller (BaseProvider.stream)
|
|
@@ -1171,24 +1214,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
1171
1214
|
logger.info("[GoogleAIStudio] Routing TTS direct-synthesis to handleDirectTTSSynthesis", { model: modelName });
|
|
1172
1215
|
return this.handleDirectTTSSynthesis(options, Date.now());
|
|
1173
1216
|
}
|
|
1174
|
-
|
|
1175
|
-
// native SDK. BaseProvider.generate() runs this preprocessing via
|
|
1176
|
-
// buildMultimodalMessagesArray, but AI Studio's override skips it,
|
|
1177
|
-
// which would otherwise drop text-file content (and the
|
|
1178
|
-
// mimetype-hint contract) on the floor. Mutates options.input.text /
|
|
1179
|
-
// options.input.images / options.input.pdfFiles in place.
|
|
1180
|
-
if (options.input?.files && options.input.files.length > 0) {
|
|
1181
|
-
try {
|
|
1182
|
-
await processUnifiedFilesArray(options, 100 * 1024 * 1024, this.providerName);
|
|
1183
|
-
}
|
|
1184
|
-
catch (fileError) {
|
|
1185
|
-
logger.warn(`[GoogleAIStudio] processUnifiedFilesArray threw, continuing without file content: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
// Runs even without input.files: a caller can populate input.images
|
|
1189
|
-
// directly, and this native path never reaches the shared multimodal
|
|
1190
|
-
// builder that would otherwise normalize the formats.
|
|
1191
|
-
await normalizeVisionImageFormats(options.input);
|
|
1217
|
+
await this.preprocessNativeFileInput(options);
|
|
1192
1218
|
// Merge registered (built-in / MCP) tools with caller-supplied tools.
|
|
1193
1219
|
// AI Studio's generate() bypasses BaseProvider.generate(), so the
|
|
1194
1220
|
// ToolsManager-driven merge that normally injects sdk.registerTool()
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* This module extracts the functions that are duplicated between the two
|
|
9
9
|
* providers so they can share a single implementation.
|
|
10
10
|
*/
|
|
11
|
-
import type { GenerateStopReason, ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput } from "../../types/index.js";
|
|
11
|
+
import type { GenerateStopReason, ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
|
|
12
12
|
import type { Tool } from "../../types/index.js";
|
|
13
13
|
/**
|
|
14
14
|
* A per-turn tool execute map that deduplicates identical tool calls.
|
|
@@ -465,4 +465,25 @@ export declare function prependConversationMessages(contents: Array<{
|
|
|
465
465
|
* is skipped rather than aborting the entire request, matching prior
|
|
466
466
|
* Vertex behaviour.
|
|
467
467
|
*/
|
|
468
|
+
/**
|
|
469
|
+
* Append audio to a Gemini request as `inlineData` parts.
|
|
470
|
+
*
|
|
471
|
+
* Shared by both Gemini front ends. Vertex assembles its request here and AI
|
|
472
|
+
* Studio assembles it in `buildUserPartsWithMultimodal`; when this lived only in
|
|
473
|
+
* the Vertex client, AI Studio advertised audio support through
|
|
474
|
+
* `NATIVE_AUDIO_PROVIDERS` and then silently dropped the bytes.
|
|
475
|
+
*
|
|
476
|
+
* Gemini's native request shape is assembled directly rather than taken from the
|
|
477
|
+
* AI SDK's `file` parts, so audio has to be added explicitly the same way PDFs
|
|
478
|
+
* and images are — a `{ type: "file" }` part built upstream simply never
|
|
479
|
+
* reaches this request body. That asymmetry is why attaching a recording
|
|
480
|
+
* produced only the metadata summary even after the message builder learned to
|
|
481
|
+
* carry the bytes.
|
|
482
|
+
*
|
|
483
|
+
* A container Gemini does not accept is converted first; one that cannot be
|
|
484
|
+
* converted is skipped rather than sent, because an unsupported inlineData
|
|
485
|
+
* mimeType fails the whole request, and the caller still has the metadata
|
|
486
|
+
* summary in the text part.
|
|
487
|
+
*/
|
|
488
|
+
export declare function appendNativeAudioParts(userParts: VertexNativePart[], audioFiles: MultimodalAudioEntry[] | undefined, logPrefix?: string): Promise<void>;
|
|
468
489
|
export declare function buildUserPartsWithMultimodal(input: GeminiMultimodalInput | undefined, textOverride?: string, logPrefix?: string): Promise<VertexNativePart[]>;
|