@juspay/neurolink 12.28.0 → 12.28.2
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 +3 -3
- package/dist/adapters/imageFormatSupport.d.ts +115 -5
- package/dist/adapters/imageFormatSupport.js +424 -12
- package/dist/browser/neurolink.min.js +368 -366
- package/dist/types/file.d.ts +39 -0
- package/dist/utils/messageBuilder.js +186 -17
- package/package.json +4 -2
package/dist/types/file.d.ts
CHANGED
|
@@ -32,6 +32,45 @@ export type VisionImageConversion = {
|
|
|
32
32
|
/** True when the bytes were re-encoded; false when they were left alone. */
|
|
33
33
|
readonly converted: boolean;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Knobs for one vision-compatibility pass.
|
|
37
|
+
*
|
|
38
|
+
* `autoOrient` exists so a caller that also wants EXIF orientation applied
|
|
39
|
+
* gets it inside the same decode. Rotating first and transcoding second means
|
|
40
|
+
* two full decode/re-encode cycles, and for the lossy formats in both sets
|
|
41
|
+
* (TIFF, which sharp writes JPEG-compressed by default) the intermediate
|
|
42
|
+
* re-encode costs a generation of image quality that the model then reads.
|
|
43
|
+
*
|
|
44
|
+
* `outputFormat` picks the transcode target; see {@link VisionImageOutputFormat}
|
|
45
|
+
* for the default and why it stays PNG unless a caller opts into JPEG.
|
|
46
|
+
*/
|
|
47
|
+
export type VisionTranscodeOptions = {
|
|
48
|
+
readonly autoOrient?: boolean;
|
|
49
|
+
readonly outputFormat?: VisionImageOutputFormat;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* What a bounded header probe could establish about an image's EXIF
|
|
53
|
+
* orientation tag, without decoding the image or reading all of its bytes.
|
|
54
|
+
*
|
|
55
|
+
* `"inconclusive"` is not a failure — it is the honest answer whenever the
|
|
56
|
+
* prefix ran out before the parse reached a verdict, or the container is one
|
|
57
|
+
* this probe does not parse. Callers must treat it as "find out the expensive
|
|
58
|
+
* way", never as "no tag".
|
|
59
|
+
*/
|
|
60
|
+
export type ExifOrientationProbe = "absent" | "present" | "inconclusive";
|
|
61
|
+
/**
|
|
62
|
+
* Outcome of an EXIF-orientation pass over one image.
|
|
63
|
+
*
|
|
64
|
+
* See `adapters/imageFormatSupport.ts` — `normalized` is false both when the
|
|
65
|
+
* image carried no orientation tag (or an already-upright `1`) and when the
|
|
66
|
+
* pass could not run (sharp unavailable, decode failure), so callers must not
|
|
67
|
+
* treat it as a success flag; either way `buffer` is safe to send as-is.
|
|
68
|
+
*/
|
|
69
|
+
export type ImageOrientationNormalization = {
|
|
70
|
+
readonly buffer: Buffer;
|
|
71
|
+
/** True when the bytes were re-encoded to apply and strip EXIF orientation. */
|
|
72
|
+
readonly normalized: boolean;
|
|
73
|
+
};
|
|
35
74
|
/**
|
|
36
75
|
* Outcome of an audio-compatibility pass over one file.
|
|
37
76
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
|
-
import { readFile as readFileAsync, stat as statAsync } from "fs/promises";
|
|
2
|
+
import { open as openAsync, readFile as readFileAsync, stat as statAsync, } from "fs/promises";
|
|
3
3
|
import pLimit from "p-limit";
|
|
4
4
|
import { request } from "undici";
|
|
5
5
|
import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
|
|
@@ -8,9 +8,9 @@ import { CONVERSATION_INSTRUCTIONS, STRUCTURED_OUTPUT_INSTRUCTIONS, } from "../c
|
|
|
8
8
|
import { getAvailableInputTokens } from "../constants/contextWindows.js";
|
|
9
9
|
import { PDF_LIMITS } from "../core/constants.js";
|
|
10
10
|
import { enforceAggregateFileBudget, FILE_READ_BUDGET_PERCENT, } from "../context/fileTokenBudget.js";
|
|
11
|
-
import { isCSVContent, SIZE_TIER_THRESHOLDS } from "../types/index.js";
|
|
11
|
+
import { isCSVContent, isTextContent, SIZE_TIER_THRESHOLDS, } from "../types/index.js";
|
|
12
12
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
13
|
-
import { needsVisionTranscode, toVisionCompatibleImage, } from "../adapters/imageFormatSupport.js";
|
|
13
|
+
import { EXIF_PROBE_PREFIX_BYTES, mayCarryExifOrientation, needsVisionTranscode, normalizeImageOrientation, probeExifOrientation, toVisionCompatibleImage, } from "../adapters/imageFormatSupport.js";
|
|
14
14
|
import { FILE_TYPE_REGISTRY, lookupByExtension, } from "../processors/config/fileTypeRegistry.js";
|
|
15
15
|
import { ErrorFactory, NeuroLinkError, withTimeout } from "./errorHandling.js";
|
|
16
16
|
import { FileDetector } from "./fileDetector.js";
|
|
@@ -1405,6 +1405,20 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1405
1405
|
supportsNativeVideo(provider, model);
|
|
1406
1406
|
// If no images, PDFs, audio or video, use standard message building and convert to MultimodalChatMessage[]
|
|
1407
1407
|
if (!hasImages && !hasPDFs && !hasNativeAudio && !hasNativeVideo) {
|
|
1408
|
+
// A `content: [{type: "text", ...}]` item never reaches
|
|
1409
|
+
// convertContentToProviderFormat on this branch (that only runs for the
|
|
1410
|
+
// multimodal path below), and buildMessagesArray only ever reads
|
|
1411
|
+
// inp.text/options.prompt — so without this fold, every TextContent item
|
|
1412
|
+
// in `content[]` was silently dropped whenever the request carried no
|
|
1413
|
+
// image, PDF, native audio or native video. Fold them into inp.text first
|
|
1414
|
+
// (same append-and-preserve-order shape as the CSV fold immediately
|
|
1415
|
+
// below), so ordinary text-only requests behave the same as the
|
|
1416
|
+
// multimodal path, which already reads its (first) text item out of
|
|
1417
|
+
// `content[]`.
|
|
1418
|
+
const textContentItems = inp.content?.filter(isTextContent) ?? [];
|
|
1419
|
+
if (textContentItems.length > 0) {
|
|
1420
|
+
inp.text = appendTextContentToText(textContentItems, inp.text ?? "");
|
|
1421
|
+
}
|
|
1408
1422
|
// #289: CSV content[] items don't need vision, so they never reach the
|
|
1409
1423
|
// multimodal converter below — process them into the prompt text here
|
|
1410
1424
|
// (otherwise a `content: [{type:"csv"}]`-only request silently drops it).
|
|
@@ -1562,6 +1576,23 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1562
1576
|
throw error;
|
|
1563
1577
|
}
|
|
1564
1578
|
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Fold TextContent `content[]` items into the prompt text, in the order they
|
|
1581
|
+
* appear, on the text-only path (no image/PDF/native-audio, so
|
|
1582
|
+
* convertContentToProviderFormat's own text handling never runs). Mirrors
|
|
1583
|
+
* appendCsvContentToText's append-after-base-text shape; unlike CSV this
|
|
1584
|
+
* needs no async decoding, just string concatenation.
|
|
1585
|
+
*/
|
|
1586
|
+
function appendTextContentToText(textItems, baseText) {
|
|
1587
|
+
let text = baseText;
|
|
1588
|
+
for (const item of textItems) {
|
|
1589
|
+
if (!item.text) {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
text += `${text ? "\n\n" : ""}${item.text}`;
|
|
1593
|
+
}
|
|
1594
|
+
return text;
|
|
1595
|
+
}
|
|
1565
1596
|
/**
|
|
1566
1597
|
* Timeout for detecting/parsing an in-memory CSV `content[]` buffer (#325
|
|
1567
1598
|
* review, round 2). Bounds a stalled detector rather than blocking the
|
|
@@ -1976,12 +2007,17 @@ async function processImageToBase64(image, index, outputFormat) {
|
|
|
1976
2007
|
ImageProcessor.validateBufferSize(image, `image input at index ${index}`);
|
|
1977
2008
|
imageData = image.toString("base64");
|
|
1978
2009
|
}
|
|
1979
|
-
// Last line of defence for vision-format compatibility.
|
|
1980
|
-
// handles `input.images` eagerly so the
|
|
1981
|
-
// directly (Google AI Studio, Bedrock) see
|
|
1982
|
-
// arrive as URLs are downloaded
|
|
1983
|
-
// here. A no-op for
|
|
1984
|
-
|
|
2010
|
+
// Last line of defence for vision-format compatibility AND orientation.
|
|
2011
|
+
// `normalizeVisionImageFormats` handles `input.images` eagerly so the
|
|
2012
|
+
// providers that read that array directly (Google AI Studio, Bedrock) see
|
|
2013
|
+
// converted/oriented bytes, but images that arrive as URLs are downloaded
|
|
2014
|
+
// further downstream and only become bytes here. A no-op for PNG/GIF (which
|
|
2015
|
+
// cannot carry an orientation tag) already sent as-is, so the common
|
|
2016
|
+
// no-EXIF case is unaffected.
|
|
2017
|
+
const needsOrientationCheck = mayCarryExifOrientation(mimeType);
|
|
2018
|
+
const needsTranscode = needsVisionTranscode(mimeType);
|
|
2019
|
+
const context = `image input at index ${index}`;
|
|
2020
|
+
if (needsTranscode) {
|
|
1985
2021
|
// Guard the decoded bytes, not just the Buffer input. The buffer branch
|
|
1986
2022
|
// above is already checked, but a data: URI reaches here having only been
|
|
1987
2023
|
// regex-matched — so an oversized one was handed straight to sharp/ffmpeg,
|
|
@@ -1989,15 +2025,41 @@ async function processImageToBase64(image, index, outputFormat) {
|
|
|
1989
2025
|
//
|
|
1990
2026
|
// Sized before the decode rather than after: checking the Buffer would mean
|
|
1991
2027
|
// allocating the very thing the limit exists to refuse.
|
|
1992
|
-
const context = `image input at index ${index}`;
|
|
1993
2028
|
ImageProcessor.validateSize(base64DecodedByteLength(imageData), context);
|
|
1994
2029
|
const rawImage = Buffer.from(imageData, "base64");
|
|
1995
|
-
|
|
2030
|
+
// Both passes need the same decode, so a format in both sets (HEIC,
|
|
2031
|
+
// HEIF, TIFF, AVIF) gets one pipeline rather than two — see
|
|
2032
|
+
// `toVisionCompatibleImage`'s `autoOrient`.
|
|
2033
|
+
const compatible = await toVisionCompatibleImage(rawImage, mimeType, {
|
|
2034
|
+
autoOrient: needsOrientationCheck,
|
|
2035
|
+
outputFormat,
|
|
2036
|
+
});
|
|
1996
2037
|
if (compatible.converted) {
|
|
1997
2038
|
imageData = compatible.buffer.toString("base64");
|
|
1998
2039
|
mimeType = compatible.mimeType;
|
|
1999
2040
|
}
|
|
2000
2041
|
}
|
|
2042
|
+
else if (needsOrientationCheck) {
|
|
2043
|
+
// Ask a bounded header prefix whether this image needs touching at all
|
|
2044
|
+
// before paying for the size guard or a full decode. PNG/JPEG/WebP were
|
|
2045
|
+
// previously gated on `needsTranscode` alone — false for all three — so
|
|
2046
|
+
// this MIME type reached the provider with no local size check
|
|
2047
|
+
// whatsoever. A large image with no orientation tag is never going to be
|
|
2048
|
+
// modified by this pass, and must keep reaching the provider exactly as
|
|
2049
|
+
// it did before rather than being rejected locally for a limit that
|
|
2050
|
+
// exists to bound a decode nothing here needs.
|
|
2051
|
+
if (probeBase64ExifOrientation(imageData, mimeType) !== "absent") {
|
|
2052
|
+
ImageProcessor.validateSize(base64DecodedByteLength(imageData), context);
|
|
2053
|
+
const rawImage = Buffer.from(imageData, "base64");
|
|
2054
|
+
const oriented = await normalizeImageOrientation(rawImage, mimeType);
|
|
2055
|
+
// Re-encoding changes byte length. An oversized result falls back to
|
|
2056
|
+
// the already-validated original, matching normalizeVisionImageFormats.
|
|
2057
|
+
if (oriented.normalized &&
|
|
2058
|
+
withinConversionLimit(oriented.buffer, `oriented ${context}`)) {
|
|
2059
|
+
imageData = oriented.buffer.toString("base64");
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2001
2063
|
return { imageData, mimeType };
|
|
2002
2064
|
}
|
|
2003
2065
|
/**
|
|
@@ -2027,14 +2089,46 @@ export async function normalizeVisionImageFormats(input, outputFormat) {
|
|
|
2027
2089
|
? entry.data
|
|
2028
2090
|
: entry;
|
|
2029
2091
|
const source = await readImageSourceForConversion(payload);
|
|
2030
|
-
if (!source ||
|
|
2092
|
+
if (!source ||
|
|
2093
|
+
(!needsVisionTranscode(source.mimeType) &&
|
|
2094
|
+
!mayCarryExifOrientation(source.mimeType))) {
|
|
2031
2095
|
continue;
|
|
2032
2096
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2097
|
+
let buffer = source.buffer;
|
|
2098
|
+
let mimeType = source.mimeType;
|
|
2099
|
+
let changed = false;
|
|
2100
|
+
const orientable = mayCarryExifOrientation(mimeType);
|
|
2101
|
+
if (needsVisionTranscode(mimeType)) {
|
|
2102
|
+
// A format in both sets (HEIC, HEIF, TIFF, AVIF) is oriented inside the
|
|
2103
|
+
// transcode's own decode instead of ahead of it. Two sequential passes
|
|
2104
|
+
// cost two decodes and two encodes, and the intermediate re-encode is
|
|
2105
|
+
// lossy for some of these formats — so the model would read an image a
|
|
2106
|
+
// generation worse than the one it could have been sent.
|
|
2107
|
+
const compatible = await toVisionCompatibleImage(buffer, mimeType, {
|
|
2108
|
+
autoOrient: orientable,
|
|
2109
|
+
outputFormat,
|
|
2110
|
+
});
|
|
2111
|
+
if (compatible.converted) {
|
|
2112
|
+
buffer = compatible.buffer;
|
|
2113
|
+
mimeType = compatible.mimeType;
|
|
2114
|
+
changed = true;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
else if (orientable) {
|
|
2118
|
+
const oriented = await normalizeImageOrientation(buffer, mimeType);
|
|
2119
|
+
// Re-encoding changes byte length, so the size guard that gated the
|
|
2120
|
+
// read above must run again on the result — a re-encode that grew past
|
|
2121
|
+
// the limit must not silently replace an under-limit original.
|
|
2122
|
+
if (oriented.normalized &&
|
|
2123
|
+
withinConversionLimit(oriented.buffer, `oriented image at index ${index}`)) {
|
|
2124
|
+
buffer = oriented.buffer;
|
|
2125
|
+
changed = true;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (!changed) {
|
|
2035
2129
|
continue;
|
|
2036
2130
|
}
|
|
2037
|
-
const dataUri = `data:${
|
|
2131
|
+
const dataUri = `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
2038
2132
|
images[index] = isWrapped
|
|
2039
2133
|
? { ...entry, data: dataUri }
|
|
2040
2134
|
: dataUri;
|
|
@@ -2081,6 +2175,34 @@ function base64DecodedByteLength(base64) {
|
|
|
2081
2175
|
}
|
|
2082
2176
|
return Math.max(0, Math.floor(significant / 4) * 3 - padding);
|
|
2083
2177
|
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Base64 characters sufficient to decode at least
|
|
2180
|
+
* {@link EXIF_PROBE_PREFIX_BYTES} bytes, rounded up to a 4-character group so
|
|
2181
|
+
* the slice below always starts at the beginning of the encoded stream.
|
|
2182
|
+
*/
|
|
2183
|
+
const EXIF_PROBE_BASE64_CHARS = Math.ceil(EXIF_PROBE_PREFIX_BYTES / 3) * 4;
|
|
2184
|
+
/**
|
|
2185
|
+
* `probeExifOrientation`, but for a still-encoded data-URI payload: decodes
|
|
2186
|
+
* only its leading {@link EXIF_PROBE_BASE64_CHARS} characters rather than the
|
|
2187
|
+
* whole string, the same bounded-prefix trade `probeFileExifOrientation`
|
|
2188
|
+
* makes for a local file. This is what lets `processImageToBase64` ask "does
|
|
2189
|
+
* this image need touching at all?" before it pays for the size guard or a
|
|
2190
|
+
* full decode — see the call site for why that ordering is the point.
|
|
2191
|
+
*
|
|
2192
|
+
* Reports `"inconclusive"` on any decode failure rather than propagating it:
|
|
2193
|
+
* the caller's full-size path handles that verdict correctly already.
|
|
2194
|
+
*/
|
|
2195
|
+
function probeBase64ExifOrientation(base64, mimeType) {
|
|
2196
|
+
try {
|
|
2197
|
+
const window = base64.length <= EXIF_PROBE_BASE64_CHARS
|
|
2198
|
+
? base64
|
|
2199
|
+
: base64.slice(0, EXIF_PROBE_BASE64_CHARS);
|
|
2200
|
+
return probeExifOrientation(Buffer.from(window, "base64"), mimeType);
|
|
2201
|
+
}
|
|
2202
|
+
catch {
|
|
2203
|
+
return "inconclusive";
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2084
2206
|
/**
|
|
2085
2207
|
* Whether a payload is small enough to hand to an image decoder, reported
|
|
2086
2208
|
* rather than thrown. See {@link readImageSourceForConversion} for why this
|
|
@@ -2100,6 +2222,42 @@ function withinConversionByteLimit(bytes, context) {
|
|
|
2100
2222
|
function withinConversionLimit(buffer, context) {
|
|
2101
2223
|
return withinConversionByteLimit(buffer.length, context);
|
|
2102
2224
|
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Ask a file's own header bytes whether it carries an EXIF orientation,
|
|
2227
|
+
* reading at most {@link EXIF_PROBE_PREFIX_BYTES} to find out.
|
|
2228
|
+
*
|
|
2229
|
+
* This is what keeps the orientation pass off the JPEG hot path. Without it,
|
|
2230
|
+
* widening the conversion gate to cover orientation means every local `.jpg`
|
|
2231
|
+
* and `.webp` is stat'd, read into memory in full and handed to a decoder,
|
|
2232
|
+
* purely to discover that the overwhelming majority carry no tag — new I/O
|
|
2233
|
+
* on the single most common image input there is.
|
|
2234
|
+
*
|
|
2235
|
+
* Reports `"inconclusive"` on any read failure rather than propagating it:
|
|
2236
|
+
* the full read that follows will hit the same problem a few lines later,
|
|
2237
|
+
* where the existing handler already turns it into a warning and a skipped
|
|
2238
|
+
* conversion. Failing here instead would only duplicate that.
|
|
2239
|
+
*/
|
|
2240
|
+
async function probeFileExifOrientation(path, mimeType, size) {
|
|
2241
|
+
// The caller has already stat'd, so a small file costs a small buffer
|
|
2242
|
+
// rather than the window's full width.
|
|
2243
|
+
const window = Math.min(Math.max(size, 0), EXIF_PROBE_PREFIX_BYTES);
|
|
2244
|
+
if (window === 0) {
|
|
2245
|
+
return "inconclusive";
|
|
2246
|
+
}
|
|
2247
|
+
let handle;
|
|
2248
|
+
try {
|
|
2249
|
+
handle = await openAsync(path, "r");
|
|
2250
|
+
const prefix = Buffer.alloc(window);
|
|
2251
|
+
const { bytesRead } = await handle.read(prefix, 0, prefix.length, 0);
|
|
2252
|
+
return probeExifOrientation(prefix.subarray(0, bytesRead), mimeType);
|
|
2253
|
+
}
|
|
2254
|
+
catch {
|
|
2255
|
+
return "inconclusive";
|
|
2256
|
+
}
|
|
2257
|
+
finally {
|
|
2258
|
+
await handle?.close().catch(() => undefined);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2103
2261
|
async function readImageSourceForConversion(payload) {
|
|
2104
2262
|
// Both in-memory shapes are size-checked before they can reach a decoder,
|
|
2105
2263
|
// the same way the file-path branch below is. `withinConversionLimit`
|
|
@@ -2131,16 +2289,27 @@ async function readImageSourceForConversion(payload) {
|
|
|
2131
2289
|
return undefined;
|
|
2132
2290
|
}
|
|
2133
2291
|
const mimeType = getMimeTypeFromExtension(payload);
|
|
2134
|
-
|
|
2292
|
+
const transcodable = needsVisionTranscode(mimeType);
|
|
2293
|
+
if (!transcodable && !mayCarryExifOrientation(mimeType)) {
|
|
2135
2294
|
return undefined;
|
|
2136
2295
|
}
|
|
2137
2296
|
try {
|
|
2138
2297
|
// Preflight the size before reading. Conversion replaces the entry with a
|
|
2139
2298
|
// data URI, which bypasses processImageToBase64's buffer-size guard — so
|
|
2140
2299
|
// without this a large local HEIC/TIFF/BMP was read fully into memory and
|
|
2141
|
-
// then re-encoded, with no limit applied at either step.
|
|
2300
|
+
// then re-encoded, with no limit applied at either step. The same applies
|
|
2301
|
+
// to a local JPEG read only for its EXIF tag: still bounded before the read.
|
|
2142
2302
|
const { size } = await statAsync(payload);
|
|
2143
2303
|
ImageProcessor.validateSize(size, `image at ${safeBasename(payload)}`);
|
|
2304
|
+
// A file whose only reason to be here is the orientation question gets
|
|
2305
|
+
// that question answered from its header, and is not read at all when the
|
|
2306
|
+
// answer is no. A transcodable format is exempt: its bytes are needed
|
|
2307
|
+
// whatever the orientation turns out to be, so a probe would be pure
|
|
2308
|
+
// overhead.
|
|
2309
|
+
if (!transcodable &&
|
|
2310
|
+
(await probeFileExifOrientation(payload, mimeType, size)) === "absent") {
|
|
2311
|
+
return undefined;
|
|
2312
|
+
}
|
|
2144
2313
|
const buffer = await readFileAsync(payload);
|
|
2145
2314
|
ImageProcessor.validateBufferSize(buffer, `image at ${safeBasename(payload)}`);
|
|
2146
2315
|
return { buffer, mimeType };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.28.
|
|
3
|
+
"version": "12.28.2",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"test:media-registry-collisions": "pnpm exec tsx test/continuous-test-suite-media-registry-collisions.ts",
|
|
97
97
|
"test:memory": "pnpm exec tsx test/continuous-test-suite-memory.ts",
|
|
98
98
|
"test:multimodal-rag": "pnpm exec tsx test/continuous-test-suite-multimodal-rag.ts",
|
|
99
|
+
"test:multimodal-text-content-fold": "pnpm exec tsx test/continuous-test-suite-multimodal-text-content-fold.ts",
|
|
99
100
|
"test:proxy-sharing": "pnpm run build && pnpm exec tsx test/continuous-test-suite-proxy-sharing.ts",
|
|
100
101
|
"test:openai-compat-streaming-retry": "pnpm exec tsx test/continuous-test-suite-openai-compat-streaming-retry.ts",
|
|
101
102
|
"test:anthropic-streaming-retry": "pnpm exec tsx test/continuous-test-suite-anthropic-streaming-retry.ts",
|
|
@@ -183,7 +184,7 @@
|
|
|
183
184
|
"// CI tier — fast, no live AI calls, safe for every commit (test:unit; also see the separate provider-safety-net CI job, which runs build + test:providers-mocked + test:provider-structure + test:error-classifier-contract on every PR)": "",
|
|
184
185
|
"test:tool-routing": "pnpm exec tsx test/continuous-test-suite-tool-routing.ts",
|
|
185
186
|
"test:tool-routing-semantic": "pnpm exec tsx test/continuous-test-suite-tool-routing-semantic.ts",
|
|
186
|
-
"test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:classifier-router && pnpm run test:tool-routing-semantic && pnpm run test:mcp-result-cache && pnpm run test:mcp-direct-name-repair && pnpm run test:mcp-breaker-resolved-errors && pnpm run test:model-not-found-retryable && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:vector-chroma && pnpm run test:vector-pgvector && pnpm run test:vector-pinecone && pnpm run test:provider-wiring && pnpm run test:docs-mcp",
|
|
187
|
+
"test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:classifier-router && pnpm run test:tool-routing-semantic && pnpm run test:mcp-result-cache && pnpm run test:mcp-direct-name-repair && pnpm run test:mcp-breaker-resolved-errors && pnpm run test:model-not-found-retryable && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:image-exif && pnpm run test:vector-chroma && pnpm run test:vector-pgvector && pnpm run test:vector-pinecone && pnpm run test:provider-wiring && pnpm run test:docs-mcp",
|
|
187
188
|
"// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit; test:matrix, a different suite covering the full provider capability matrix, runs nightly via .github/workflows/live-matrix.yml — test:providers itself is still only wired into test:live, not any GitHub Actions workflow)": "",
|
|
188
189
|
"test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
|
|
189
190
|
"// CI tier — product output (image/video/TTS/PPT) — costs $$ per run (not wired into any GitHub Actions workflow as of this comment; run manually or add to live-matrix.yml if nightly coverage is needed)": "",
|
|
@@ -250,6 +251,7 @@
|
|
|
250
251
|
"test:archive:security": "pnpm exec tsx test/continuous-test-suite-archive-security.ts",
|
|
251
252
|
"test:office:security": "pnpm exec tsx test/continuous-test-suite-office-security.ts",
|
|
252
253
|
"test:vision-image-format": "pnpm exec tsx test/continuous-test-suite-vision-image-format.ts",
|
|
254
|
+
"test:image-exif": "pnpm exec tsx test/continuous-test-suite-image-exif.ts",
|
|
253
255
|
"test:model-pool": "pnpm exec tsx test/continuous-test-suite-model-pool.ts",
|
|
254
256
|
"test:agents": "pnpm exec tsx test/continuous-test-suite-agents.ts",
|
|
255
257
|
"test:classifier-router": "pnpm exec tsx test/continuous-test-suite-classifier-router.ts",
|