@juspay/neurolink 12.19.2 → 12.20.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 -2
- package/dist/adapters/imageFormatSupport.d.ts +5 -2
- package/dist/adapters/imageFormatSupport.js +47 -24
- package/dist/browser/neurolink.min.js +359 -359
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/core/modules/MessageBuilder.js +2 -0
- package/dist/neurolink.js +1 -0
- package/dist/processors/config/fileExtensions.d.ts +8 -1
- package/dist/processors/config/fileExtensions.js +8 -1
- package/dist/rag/pipeline/RAGPipeline.js +29 -0
- package/dist/rag/ragIntegration.js +21 -1
- package/dist/types/file.d.ts +10 -0
- package/dist/types/generate.d.ts +27 -1
- package/dist/types/stream.d.ts +13 -1
- package/dist/utils/imageProcessor.js +151 -6
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +23 -13
- package/dist/utils/multimodalOptionsBuilder.d.ts +3 -0
- package/dist/utils/multimodalOptionsBuilder.js +1 -0
- package/package.json +2 -1
|
@@ -4,4 +4,4 @@ import type { OptionSchema, TextGenerationOptions } from "../../types/index.js";
|
|
|
4
4
|
* This object provides metadata for validation and help text in the CLI loop.
|
|
5
5
|
* It is derived from the main TextGenerationOptions interface to ensure consistency.
|
|
6
6
|
*/
|
|
7
|
-
export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation" | "toolExecutionCapture" | "toolExecutionRecorder">, OptionSchema>;
|
|
7
|
+
export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "imageOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation" | "toolExecutionCapture" | "toolExecutionRecorder">, OptionSchema>;
|
|
@@ -104,6 +104,7 @@ export class MessageBuilder {
|
|
|
104
104
|
},
|
|
105
105
|
csvOptions: options.csvOptions,
|
|
106
106
|
pdfOptions: options.pdfOptions,
|
|
107
|
+
imageOptions: options.imageOptions,
|
|
107
108
|
provider: options.provider,
|
|
108
109
|
model: options.model,
|
|
109
110
|
temperature: options.temperature,
|
|
@@ -226,6 +227,7 @@ export class MessageBuilder {
|
|
|
226
227
|
},
|
|
227
228
|
csvOptions: options.csvOptions,
|
|
228
229
|
pdfOptions: options.pdfOptions,
|
|
230
|
+
imageOptions: options.imageOptions,
|
|
229
231
|
provider: options.provider,
|
|
230
232
|
model: options.model,
|
|
231
233
|
temperature: options.temperature,
|
package/dist/neurolink.js
CHANGED
|
@@ -4210,6 +4210,7 @@ Current user's request: ${currentInput}`;
|
|
|
4210
4210
|
// reached convertToImages.
|
|
4211
4211
|
csvOptions: options.csvOptions,
|
|
4212
4212
|
pdfOptions: options.pdfOptions,
|
|
4213
|
+
imageOptions: options.imageOptions,
|
|
4213
4214
|
region: options.region,
|
|
4214
4215
|
tts: options.tts,
|
|
4215
4216
|
stt: options.stt,
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
/**
|
|
15
15
|
* Image file extensions supported by the platform.
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* Includes `.svg`. On the processor path (`SvgProcessor`) that guarantees
|
|
18
|
+
* sanitized markup, never raster bytes — but that guarantee is scoped to
|
|
19
|
+
* that one consumer. This constant is also consumed by code that treats
|
|
20
|
+
* every image extension as raster (captioning, multimodal embedding, vision
|
|
21
|
+
* adapters); `.svg` is markup there too, just without the sanitization, so
|
|
22
|
+
* each such consumer must explicitly handle or reject `.svg` itself rather
|
|
23
|
+
* than assume this list implies raster-safety.
|
|
17
24
|
*/
|
|
18
25
|
export declare const IMAGE_EXTENSIONS: readonly string[];
|
|
19
26
|
/**
|
|
@@ -17,7 +17,14 @@ import { extensionsForModality } from "./fileTypeRegistry.js";
|
|
|
17
17
|
// =============================================================================
|
|
18
18
|
/**
|
|
19
19
|
* Image file extensions supported by the platform.
|
|
20
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* Includes `.svg`. On the processor path (`SvgProcessor`) that guarantees
|
|
22
|
+
* sanitized markup, never raster bytes — but that guarantee is scoped to
|
|
23
|
+
* that one consumer. This constant is also consumed by code that treats
|
|
24
|
+
* every image extension as raster (captioning, multimodal embedding, vision
|
|
25
|
+
* adapters); `.svg` is markup there too, just without the sanitization, so
|
|
26
|
+
* each such consumer must explicitly handle or reject `.svg` itself rather
|
|
27
|
+
* than assume this list implies raster-safety.
|
|
21
28
|
*/
|
|
22
29
|
export const IMAGE_EXTENSIONS = extensionsForModality("image");
|
|
23
30
|
/**
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
import { randomUUID } from "crypto";
|
|
29
|
+
import { extname } from "path";
|
|
29
30
|
import { MDocument } from "../document/MDocument.js";
|
|
30
31
|
import { loadDocument } from "../document/loaders.js";
|
|
31
32
|
import { InMemoryVectorStore } from "../retrieval/vectorQueryTool.js";
|
|
@@ -472,6 +473,19 @@ export class RAGPipeline {
|
|
|
472
473
|
let chunksCreated = 0;
|
|
473
474
|
for (const source of sources) {
|
|
474
475
|
try {
|
|
476
|
+
if (typeof source === "string") {
|
|
477
|
+
// IMAGE_EXTENSIONS includes .svg, but that constant's raster
|
|
478
|
+
// guarantee is scoped to the processor path (SvgProcessor). Here
|
|
479
|
+
// it would flow raw markup bytes into generateImageCaption (which
|
|
480
|
+
// base64s them into a vision call) and into the multi-modal embed
|
|
481
|
+
// call — both raster-only. Mirrors the identical skip in
|
|
482
|
+
// ragIntegration.ts's ingestion-tool entry point so the two agree.
|
|
483
|
+
const withoutQueryOrFragment = source.split(/[?#]/)[0];
|
|
484
|
+
if (extname(withoutQueryOrFragment).toLowerCase() === ".svg") {
|
|
485
|
+
logger.warn(`[RAGPipeline] SVG is not supported as a multi-modal image source, skipping: ${redactUrlForError(source)}`);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
475
489
|
// Load the image
|
|
476
490
|
let imageDoc;
|
|
477
491
|
if (Buffer.isBuffer(source)) {
|
|
@@ -484,6 +498,21 @@ export class RAGPipeline {
|
|
|
484
498
|
else {
|
|
485
499
|
imageDoc = await this.imageLoader.load(source);
|
|
486
500
|
}
|
|
501
|
+
// The extension check above is necessary but not sufficient: it only
|
|
502
|
+
// sees the name. A source with no `.svg` suffix — an extension-less
|
|
503
|
+
// URL, or a local file named without one — reaches ImageLoader, which
|
|
504
|
+
// falls back to detecting the type from the bytes and returns
|
|
505
|
+
// `image/svg+xml` for markup starting `<svg`/`<?xm`. So re-check the
|
|
506
|
+
// resolved type AND the bytes: loadFromPath prefers a recognized
|
|
507
|
+
// extension over content detection, so SVG markup named `.png` keeps
|
|
508
|
+
// an image/png label. Skip rather than throw so SVG behaves the same
|
|
509
|
+
// whichever way it was identified. `supportedFormats` does not cover
|
|
510
|
+
// this: it is optional, and unset on the default config.
|
|
511
|
+
if (imageDoc.mimeType === "image/svg+xml" ||
|
|
512
|
+
ImageProcessor.detectImageType(imageDoc.image) === "image/svg+xml") {
|
|
513
|
+
logger.warn(`[RAGPipeline] SVG is not supported as a multi-modal image source, skipping: ${redactUrlForError(imageDoc.metadata.source)}`);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
487
516
|
const { supportedFormats } = this.multiModalConfig;
|
|
488
517
|
if (supportedFormats && !supportedFormats.includes(imageDoc.mimeType)) {
|
|
489
518
|
throw ErrorFactory.invalidParameters("ingestImages", new Error(`unsupported image MIME type: ${imageDoc.mimeType}`), { mimeType: imageDoc.mimeType, supportedFormats });
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../observability/index.js";
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
6
|
import { redactUrlForError } from "../utils/logSanitize.js";
|
|
7
|
+
import { ImageProcessor } from "../utils/imageProcessor.js";
|
|
7
8
|
import { createChunker } from "./ChunkerFactory.js";
|
|
8
9
|
import { createVectorQueryTool, InMemoryVectorStore, } from "./retrieval/vectorQueryTool.js";
|
|
9
10
|
import { ImageLoader } from "./document/imageLoader.js";
|
|
@@ -300,6 +301,25 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
|
|
|
300
301
|
for (const { path: imgPath } of imageFiles) {
|
|
301
302
|
try {
|
|
302
303
|
const imageDoc = await imageLoader.load(imgPath);
|
|
304
|
+
// The two `ext === ".svg"` checks above are a cheap early-out, not the
|
|
305
|
+
// boundary: both match the NAME exactly, and the name is not what
|
|
306
|
+
// decides the type. ImageLoader resolves it two ways that disagree
|
|
307
|
+
// with a bare `.svg` comparison — `.svgz` maps to image/svg+xml
|
|
308
|
+
// through EXTENSION_MIME_MAP, and loadFromURL ignores the extension
|
|
309
|
+
// entirely and sniffs the bytes, so any image-extension URL actually
|
|
310
|
+
// serving SVG resolves to image/svg+xml here. A local raster suffix
|
|
311
|
+
// has the opposite problem: loadFromPath trusts the extension before
|
|
312
|
+
// the bytes, so inspect both. Otherwise `hasImage: true` below sends
|
|
313
|
+
// SVG markup to the raster embed call. Skip rather than throw,
|
|
314
|
+
// matching the early-outs and the identical guard
|
|
315
|
+
// in RAGPipeline.ingestImages — the two entry points are independent
|
|
316
|
+
// (prepareRAGTool builds its chunks here, not via ingestImages), so
|
|
317
|
+
// each needs its own.
|
|
318
|
+
if (imageDoc.mimeType === "image/svg+xml" ||
|
|
319
|
+
ImageProcessor.detectImageType(imageDoc.image) === "image/svg+xml") {
|
|
320
|
+
logger.warn(`[RAG] SVG is not supported as a RAG image source, skipping: ${redactUrlForError(imgPath)}`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
303
323
|
// Use filename-based text representation for simple embedding
|
|
304
324
|
const imageText = imageDoc.text;
|
|
305
325
|
imageChunks.push({
|
|
@@ -476,7 +496,7 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
|
|
|
476
496
|
tool: aiTool,
|
|
477
497
|
toolName,
|
|
478
498
|
chunksIndexed: allChunks.length + imageChunks.length,
|
|
479
|
-
filesLoaded: fileContents.length +
|
|
499
|
+
filesLoaded: fileContents.length + imageChunks.length,
|
|
480
500
|
};
|
|
481
501
|
}
|
|
482
502
|
/** @internal Exported for testing only */
|
package/dist/types/file.d.ts
CHANGED
|
@@ -9,6 +9,16 @@ export type FileType = "csv" | "image" | "pdf" | "audio" | "video" | "archive" |
|
|
|
9
9
|
* Office document types
|
|
10
10
|
*/
|
|
11
11
|
export type OfficeDocumentType = "docx" | "pptx" | "xlsx";
|
|
12
|
+
/**
|
|
13
|
+
* Transcode target for a vision-incompatible image (`adapters/imageFormatSupport.ts`).
|
|
14
|
+
*
|
|
15
|
+
* Defaults to `"png"` everywhere a caller does not pick one: sources routed
|
|
16
|
+
* through that module are frequently lossless (TIFF, BMP, ICO) or already
|
|
17
|
+
* carry alpha, and a lossy re-encode of an image the model is about to read
|
|
18
|
+
* closely is the wrong default. `"jpeg"` is available for callers who know
|
|
19
|
+
* their source has no alpha and prefer the smaller payload.
|
|
20
|
+
*/
|
|
21
|
+
export type VisionImageOutputFormat = "png" | "jpeg";
|
|
12
22
|
/**
|
|
13
23
|
* Outcome of a vision-compatibility pass over one image.
|
|
14
24
|
*
|
package/dist/types/generate.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { AvatarOptions, AvatarResult } from "./avatar.js";
|
|
|
17
17
|
import type { MusicOptions, MusicResult } from "./music.js";
|
|
18
18
|
import type { StandardRecord, ValidationSchema, ZodUnknownSchema } from "./aliases.js";
|
|
19
19
|
import type { NeurolinkCredentials } from "./providers.js";
|
|
20
|
-
import type { CSVProcessorOptions, FileWithMetadata, MultimodalAudioEntry } from "./file.js";
|
|
20
|
+
import type { CSVProcessorOptions, FileWithMetadata, MultimodalAudioEntry, VisionImageOutputFormat } from "./file.js";
|
|
21
21
|
import type { WorkflowConfig } from "./workflow.js";
|
|
22
22
|
import type { Schema, Tool, ToolChoice } from "./tools.js";
|
|
23
23
|
import type { StepResult, LanguageModel } from "./providers.js";
|
|
@@ -156,6 +156,18 @@ export type GenerateOptions = {
|
|
|
156
156
|
*/
|
|
157
157
|
maxPages?: number;
|
|
158
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* Options for images that need transcoding before a vision provider can
|
|
161
|
+
* read them (HEIC, TIFF, BMP, ICO, JPEG 2000, AVIF — see
|
|
162
|
+
* `adapters/imageFormatSupport.ts`).
|
|
163
|
+
*/
|
|
164
|
+
imageOptions?: {
|
|
165
|
+
/**
|
|
166
|
+
* Transcode target for an incompatible image. Defaults to `"png"` — the
|
|
167
|
+
* module's own default, unchanged unless a caller opts in here.
|
|
168
|
+
*/
|
|
169
|
+
outputFormat?: VisionImageOutputFormat;
|
|
170
|
+
};
|
|
159
171
|
videoOptions?: {
|
|
160
172
|
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
161
173
|
frames?: number;
|
|
@@ -1394,6 +1406,20 @@ export type TextGenerationOptions = {
|
|
|
1394
1406
|
/** Max pages converted by the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_MAX_PAGES. */
|
|
1395
1407
|
maxPages?: number;
|
|
1396
1408
|
};
|
|
1409
|
+
/**
|
|
1410
|
+
* Options for images that need transcoding before a vision provider can
|
|
1411
|
+
* read them (HEIC, TIFF, BMP, ICO, JPEG 2000, AVIF — see
|
|
1412
|
+
* `adapters/imageFormatSupport.ts`). Mirrors `GenerateOptions.imageOptions`;
|
|
1413
|
+
* this is the shape that actually reaches `MessageBuilder` for providers
|
|
1414
|
+
* built on `BaseProvider`.
|
|
1415
|
+
*/
|
|
1416
|
+
imageOptions?: {
|
|
1417
|
+
/**
|
|
1418
|
+
* Transcode target for an incompatible image. Defaults to `"png"` — the
|
|
1419
|
+
* module's own default, unchanged unless a caller opts in here.
|
|
1420
|
+
*/
|
|
1421
|
+
outputFormat?: VisionImageOutputFormat;
|
|
1422
|
+
};
|
|
1397
1423
|
enableSummarization?: boolean;
|
|
1398
1424
|
/**
|
|
1399
1425
|
* Skip injecting tool schemas into the system prompt.
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { AIModelProviderConfig, NeurolinkCredentials } from "./providers.js
|
|
|
15
15
|
import type { TTSChunk, TTSOptions, TTSResult } from "./tts.js";
|
|
16
16
|
import type { STTOptions, STTResult } from "./stt.js";
|
|
17
17
|
import type { StandardRecord, ValidationSchema } from "./aliases.js";
|
|
18
|
-
import type { CSVProcessorOptions, FileWithMetadata } from "./file.js";
|
|
18
|
+
import type { CSVProcessorOptions, FileWithMetadata, VisionImageOutputFormat } from "./file.js";
|
|
19
19
|
import type { WorkflowConfig } from "./workflow.js";
|
|
20
20
|
import type { LanguageModel, StepResult } from "./providers.js";
|
|
21
21
|
import type { Tool, ToolChoice } from "./tools.js";
|
|
@@ -373,6 +373,18 @@ export type StreamOptions = {
|
|
|
373
373
|
*/
|
|
374
374
|
maxPages?: number;
|
|
375
375
|
};
|
|
376
|
+
/**
|
|
377
|
+
* Options for images that need transcoding before a vision provider can
|
|
378
|
+
* read them (HEIC, TIFF, BMP, ICO, JPEG 2000, AVIF — see
|
|
379
|
+
* `adapters/imageFormatSupport.ts`). Mirrors `GenerateOptions.imageOptions`.
|
|
380
|
+
*/
|
|
381
|
+
imageOptions?: {
|
|
382
|
+
/**
|
|
383
|
+
* Transcode target for an incompatible image. Defaults to `"png"` — the
|
|
384
|
+
* module's own default, unchanged unless a caller opts in here.
|
|
385
|
+
*/
|
|
386
|
+
outputFormat?: VisionImageOutputFormat;
|
|
387
|
+
};
|
|
376
388
|
videoOptions?: {
|
|
377
389
|
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
378
390
|
frames?: number;
|
|
@@ -65,6 +65,153 @@ const RETRYABLE_ERROR_CODES = new Set([
|
|
|
65
65
|
"ETIMEDOUT",
|
|
66
66
|
"ERR_NETWORK",
|
|
67
67
|
]);
|
|
68
|
+
/**
|
|
69
|
+
* Bytes of prologue {@link hasSvgRoot} decodes and scans.
|
|
70
|
+
*
|
|
71
|
+
* A real SVG reaches its root within a few hundred bytes, so 4KB is generous.
|
|
72
|
+
* The cap exists because scanning the whole buffer made every unrecognized
|
|
73
|
+
* binary — and every BMP/TIFF/ICO/AVIF, whose checks run after this one — pay
|
|
74
|
+
* a full UTF-8 decode, and let a crafted unterminated doctype walk every byte
|
|
75
|
+
* (measured: 322ms on 32MB, 69ms to reject a 16MB BMP).
|
|
76
|
+
*/
|
|
77
|
+
const SVG_PROLOGUE_SCAN_BYTES = 4096;
|
|
78
|
+
/**
|
|
79
|
+
* Find an SVG root after XML's permitted leading constructs.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately a discriminator, not an XML parser. Raster magic is checked
|
|
82
|
+
* first; this only distinguishes text reaching an SVG root after a UTF-8 BOM,
|
|
83
|
+
* XML whitespace, processing instructions, comments, or a doctype. The doctype
|
|
84
|
+
* scanner tracks quotes, comments/PIs, and the internal subset so a `>` or
|
|
85
|
+
* `]` inside any of them cannot expose a later `<svg` early. The root itself
|
|
86
|
+
* may carry an XML namespace prefix (`<s:svg xmlns:s="...">`), which is
|
|
87
|
+
* accepted alongside the bare `<svg` form.
|
|
88
|
+
*
|
|
89
|
+
* Scanning a bounded window makes "unterminated" ambiguous, and the two cases
|
|
90
|
+
* must resolve OPPOSITE ways:
|
|
91
|
+
*
|
|
92
|
+
* - the WINDOW ran out — the rest of the document is unreadable from here, so
|
|
93
|
+
* fail CLOSED and answer SVG. This function gates a rejection, so answering
|
|
94
|
+
* raster on an unterminated prologue would hand back the exact bypass it
|
|
95
|
+
* exists to close: prepend filler past the window and sail through as
|
|
96
|
+
* raster. A raster file never opens `<!--` or `<!doctype`, so the
|
|
97
|
+
* false-positive cost is a skipped image; the other direction is the
|
|
98
|
+
* vulnerability.
|
|
99
|
+
* - the BUFFER ran out — the whole document was read and is malformed, so it
|
|
100
|
+
* is genuinely not an SVG.
|
|
101
|
+
*/
|
|
102
|
+
function hasSvgRoot(input) {
|
|
103
|
+
const windowed = input.length > SVG_PROLOGUE_SCAN_BYTES;
|
|
104
|
+
const text = input
|
|
105
|
+
.subarray(0, Math.min(input.length, SVG_PROLOGUE_SCAN_BYTES))
|
|
106
|
+
.toString("utf8");
|
|
107
|
+
let position = text.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
108
|
+
const skipWhitespace = () => {
|
|
109
|
+
while (/\s/.test(text[position] ?? "")) {
|
|
110
|
+
position++;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const skipDoctype = () => {
|
|
114
|
+
let quote;
|
|
115
|
+
let subsetDepth = 0;
|
|
116
|
+
let index = position + 9;
|
|
117
|
+
while (index < text.length) {
|
|
118
|
+
const char = text[index];
|
|
119
|
+
if (quote) {
|
|
120
|
+
if (char === quote) {
|
|
121
|
+
quote = undefined;
|
|
122
|
+
}
|
|
123
|
+
index++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Comments and processing instructions are valid markup inside the
|
|
127
|
+
// internal subset (and, leniently, anywhere else in the declaration)
|
|
128
|
+
// and may themselves contain `]` or `>` — e.g. `<!-- ] > -->`. Skip
|
|
129
|
+
// them as opaque spans, the same way the outer scan does, so their
|
|
130
|
+
// contents can never be misread as the subset-close or doctype-end
|
|
131
|
+
// delimiter.
|
|
132
|
+
if (text.startsWith("<!--", index)) {
|
|
133
|
+
const end = text.indexOf("-->", index + 4);
|
|
134
|
+
if (end < 0) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
index = end + 3;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (text.startsWith("<?", index)) {
|
|
141
|
+
const end = text.indexOf("?>", index + 2);
|
|
142
|
+
if (end < 0) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
index = end + 2;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (char === '"' || char === "'") {
|
|
149
|
+
quote = char;
|
|
150
|
+
}
|
|
151
|
+
else if (char === "[") {
|
|
152
|
+
subsetDepth++;
|
|
153
|
+
}
|
|
154
|
+
else if (char === "]" && subsetDepth > 0) {
|
|
155
|
+
subsetDepth--;
|
|
156
|
+
}
|
|
157
|
+
else if (char === ">" && subsetDepth === 0) {
|
|
158
|
+
position = index + 1;
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
index++;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
165
|
+
skipWhitespace();
|
|
166
|
+
while (position < text.length) {
|
|
167
|
+
if (text.startsWith("<?", position)) {
|
|
168
|
+
const end = text.indexOf("?>", position + 2);
|
|
169
|
+
if (end < 0) {
|
|
170
|
+
return windowed;
|
|
171
|
+
}
|
|
172
|
+
position = end + 2;
|
|
173
|
+
}
|
|
174
|
+
else if (text.startsWith("<!--", position)) {
|
|
175
|
+
const end = text.indexOf("-->", position + 4);
|
|
176
|
+
if (end < 0) {
|
|
177
|
+
return windowed;
|
|
178
|
+
}
|
|
179
|
+
position = end + 3;
|
|
180
|
+
}
|
|
181
|
+
else if (text.slice(position, position + 9).toLowerCase() === "<!doctype") {
|
|
182
|
+
if (!skipDoctype()) {
|
|
183
|
+
return windowed;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
skipWhitespace();
|
|
190
|
+
}
|
|
191
|
+
const rest = text.slice(position);
|
|
192
|
+
// Accepts an optional XML namespace prefix ahead of the tag name — an
|
|
193
|
+
// NCName (letters/digits/`.`/`-`/`_`, not starting with a digit) followed
|
|
194
|
+
// by `:` — so `<s:svg xmlns:s="...">` matches the same way bare `<svg`
|
|
195
|
+
// does.
|
|
196
|
+
if (/^<(?:[A-Za-z_][\w.-]*:)?svg(?:[\s/>])/i.test(rest)) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
// A fixed "`<svg` plus its delimiter is 5 characters" cutoff only worked
|
|
200
|
+
// when the tag name was the literal `svg`. With a namespace prefix now
|
|
201
|
+
// accepted, the prefix is unbounded (any NCName), so no fixed length can
|
|
202
|
+
// bound "may still be mid-token" — `<s:svg` (6 chars) is just as
|
|
203
|
+
// truncatable as `<svg` (4) was. Instead, check whether `rest` could still
|
|
204
|
+
// be the START of a string this function would accept: an optional NCName
|
|
205
|
+
// prefix, optionally followed by `:` and however much of the literal
|
|
206
|
+
// `svg` has been seen so far, with nothing left over. Anything left over
|
|
207
|
+
// (a character that cannot continue the prefix, isn't `:`, or doesn't
|
|
208
|
+
// continue "svg") proves `rest` can never complete into a match, so it is
|
|
209
|
+
// safe to answer false even though the window is cut. An empty `rest` is
|
|
210
|
+
// the same ambiguity in the degenerate case (no root text observed yet).
|
|
211
|
+
const isUnresolvedPrefix = rest.length === 0 ||
|
|
212
|
+
/^<(?:[A-Za-z_][\w.-]*)?(?::(?:s(?:v(?:g)?)?)?)?$/i.test(rest);
|
|
213
|
+
return windowed && isUnresolvedPrefix;
|
|
214
|
+
}
|
|
68
215
|
/**
|
|
69
216
|
* Determines if an HTTP error is retryable based on status code
|
|
70
217
|
* Only network errors and certain HTTP status codes should be retried
|
|
@@ -452,12 +599,10 @@ export class ImageProcessor {
|
|
|
452
599
|
return "image/webp";
|
|
453
600
|
}
|
|
454
601
|
}
|
|
455
|
-
// SVG
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return "image/svg+xml";
|
|
460
|
-
}
|
|
602
|
+
// SVG is text-based and may legally put a BOM, whitespace, an XML
|
|
603
|
+
// declaration, comments, or a doctype before its root element.
|
|
604
|
+
if (hasSvgRoot(input)) {
|
|
605
|
+
return "image/svg+xml";
|
|
461
606
|
}
|
|
462
607
|
// ISO-BMFF images: "ftyp" box at offset 4, major brand at offset 8.
|
|
463
608
|
// AVIF has codec-specific major brands. HEIC uses HEVC image/sequence
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GenerateOptions, MultimodalChatMessage, StreamOptions, TextGenerationOptions } from "../types/index.js";
|
|
1
|
+
import type { GenerateOptions, MultimodalChatMessage, StreamOptions, TextGenerationOptions, VisionImageOutputFormat } from "../types/index.js";
|
|
2
2
|
import type { ModelMessage } from "../types/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Type-safe conversion from MultimodalChatMessage[] to ModelMessage[]
|
|
@@ -56,4 +56,4 @@ export declare function buildMultimodalMessagesArray(options: GenerateOptions, p
|
|
|
56
56
|
* Entries that are `http(s)` URLs are left alone — they have no bytes yet, and
|
|
57
57
|
* `processImageToBase64` converts them once they are downloaded.
|
|
58
58
|
*/
|
|
59
|
-
export declare function normalizeVisionImageFormats(input: GenerateOptions["input"]): Promise<void>;
|
|
59
|
+
export declare function normalizeVisionImageFormats(input: GenerateOptions["input"], outputFormat?: VisionImageOutputFormat): Promise<void>;
|
|
@@ -1352,7 +1352,7 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1352
1352
|
await processUnifiedFilesArray(options, maxSize, provider);
|
|
1353
1353
|
// Detection can append images (PDF page renders, video keyframes, a HEIC
|
|
1354
1354
|
// photo), so compatibility conversion has to run after it, not before.
|
|
1355
|
-
await normalizeVisionImageFormats(inp);
|
|
1355
|
+
await normalizeVisionImageFormats(inp, options.imageOptions?.outputFormat);
|
|
1356
1356
|
// Process explicit CSV files array
|
|
1357
1357
|
await processExplicitCsvFiles(options);
|
|
1358
1358
|
// Post-processing budget enforcement and token usage logging
|
|
@@ -1486,7 +1486,7 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1486
1486
|
// the multimodal converter below, so the bytes were dropped and only the
|
|
1487
1487
|
// metadata summary folded into `text` survived. Exactly the failure this
|
|
1488
1488
|
// change exists to remove, reintroduced through the other door.
|
|
1489
|
-
userContent = await convertContentToProviderFormat(inp.content, provider, model, options.pdfOptions, inp.nativeAudioFiles ?? []);
|
|
1489
|
+
userContent = await convertContentToProviderFormat(inp.content, provider, model, options.pdfOptions, inp.nativeAudioFiles ?? [], options.imageOptions?.outputFormat);
|
|
1490
1490
|
}
|
|
1491
1491
|
else if ((inp.images && inp.images.length > 0) ||
|
|
1492
1492
|
pdfFiles.length > 0 ||
|
|
@@ -1495,7 +1495,10 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1495
1495
|
// recording was dropped and only its metadata summary — already folded
|
|
1496
1496
|
// into `text` — ever reached the model.
|
|
1497
1497
|
(inp.nativeAudioFiles?.length ?? 0) > 0) {
|
|
1498
|
-
userContent = await convertMultimodalToProviderFormat(inp.text ?? "", inp.images || [], pdfFiles, provider, model,
|
|
1498
|
+
userContent = await convertMultimodalToProviderFormat(inp.text ?? "", inp.images || [], pdfFiles, provider, model, {
|
|
1499
|
+
audioFiles: inp.nativeAudioFiles ?? [],
|
|
1500
|
+
outputFormat: options.imageOptions?.outputFormat,
|
|
1501
|
+
});
|
|
1499
1502
|
}
|
|
1500
1503
|
else {
|
|
1501
1504
|
userContent = inp.text;
|
|
@@ -1571,7 +1574,7 @@ async function appendCsvContentToText(csvItems, baseText) {
|
|
|
1571
1574
|
/**
|
|
1572
1575
|
* Convert advanced content format to provider-specific format
|
|
1573
1576
|
*/
|
|
1574
|
-
async function convertContentToProviderFormat(content, provider, _model, pdfOptions, audioFiles = []) {
|
|
1577
|
+
async function convertContentToProviderFormat(content, provider, _model, pdfOptions, audioFiles = [], outputFormat) {
|
|
1575
1578
|
const textContent = content.find((c) => c.type === "text");
|
|
1576
1579
|
const imageContent = content.filter((c) => c.type === "image");
|
|
1577
1580
|
const pdfContent = content.filter((c) => c.type === "pdf");
|
|
@@ -1624,7 +1627,10 @@ async function convertContentToProviderFormat(content, provider, _model, pdfOpti
|
|
|
1624
1627
|
await enforceAggregatePdfLimits(pdfFiles, provider, {
|
|
1625
1628
|
trustSuppliedPageCounts: false,
|
|
1626
1629
|
});
|
|
1627
|
-
return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model,
|
|
1630
|
+
return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model, {
|
|
1631
|
+
audioFiles,
|
|
1632
|
+
outputFormat,
|
|
1633
|
+
});
|
|
1628
1634
|
}
|
|
1629
1635
|
/**
|
|
1630
1636
|
* Check if a string is an internet URL
|
|
@@ -1868,7 +1874,7 @@ async function convertFilePathToBase64(filePath) {
|
|
|
1868
1874
|
* Passing raw base64 avoids this because `new URL(base64string)` throws and
|
|
1869
1875
|
* the SDK treats the string as inline base64 data instead.
|
|
1870
1876
|
*/
|
|
1871
|
-
async function processImageToBase64(image, index) {
|
|
1877
|
+
async function processImageToBase64(image, index, outputFormat) {
|
|
1872
1878
|
let imageData;
|
|
1873
1879
|
let mimeType = "image/jpeg"; // Default mime type
|
|
1874
1880
|
if (typeof image === "string") {
|
|
@@ -1947,7 +1953,7 @@ async function processImageToBase64(image, index) {
|
|
|
1947
1953
|
const context = `image input at index ${index}`;
|
|
1948
1954
|
ImageProcessor.validateSize(base64DecodedByteLength(imageData), context);
|
|
1949
1955
|
const rawImage = Buffer.from(imageData, "base64");
|
|
1950
|
-
const compatible = await toVisionCompatibleImage(rawImage, mimeType);
|
|
1956
|
+
const compatible = await toVisionCompatibleImage(rawImage, mimeType, outputFormat);
|
|
1951
1957
|
if (compatible.converted) {
|
|
1952
1958
|
imageData = compatible.buffer.toString("base64");
|
|
1953
1959
|
mimeType = compatible.mimeType;
|
|
@@ -1968,7 +1974,7 @@ async function processImageToBase64(image, index) {
|
|
|
1968
1974
|
* Entries that are `http(s)` URLs are left alone — they have no bytes yet, and
|
|
1969
1975
|
* `processImageToBase64` converts them once they are downloaded.
|
|
1970
1976
|
*/
|
|
1971
|
-
export async function normalizeVisionImageFormats(input) {
|
|
1977
|
+
export async function normalizeVisionImageFormats(input, outputFormat) {
|
|
1972
1978
|
const images = input?.images;
|
|
1973
1979
|
if (!images || images.length === 0) {
|
|
1974
1980
|
return;
|
|
@@ -1985,7 +1991,7 @@ export async function normalizeVisionImageFormats(input) {
|
|
|
1985
1991
|
if (!source || !needsVisionTranscode(source.mimeType)) {
|
|
1986
1992
|
continue;
|
|
1987
1993
|
}
|
|
1988
|
-
const compatible = await toVisionCompatibleImage(source.buffer, source.mimeType);
|
|
1994
|
+
const compatible = await toVisionCompatibleImage(source.buffer, source.mimeType, outputFormat);
|
|
1989
1995
|
if (!compatible.converted) {
|
|
1990
1996
|
continue;
|
|
1991
1997
|
}
|
|
@@ -2115,7 +2121,7 @@ async function readImageSourceForConversion(payload) {
|
|
|
2115
2121
|
* - Buffers/Data URIs: Processed normally
|
|
2116
2122
|
* - Supports alt text for accessibility (included as context in text parts)
|
|
2117
2123
|
*/
|
|
2118
|
-
async function convertSimpleImagesToProviderFormat(text, images, provider, _model) {
|
|
2124
|
+
async function convertSimpleImagesToProviderFormat(text, images, provider, _model, outputFormat) {
|
|
2119
2125
|
// Validate image count against provider-specific limits before processing
|
|
2120
2126
|
ProviderImageAdapter.validateImageCount(images.length, provider, _model);
|
|
2121
2127
|
// For Vercel AI SDK, we need to return the content in the standard format
|
|
@@ -2172,7 +2178,7 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
|
|
|
2172
2178
|
for (const [index, { data: image }] of actualImages.entries()) {
|
|
2173
2179
|
try {
|
|
2174
2180
|
// Use helper function to process image and reduce nesting depth
|
|
2175
|
-
const { imageData, mimeType } = await processImageToBase64(image, index);
|
|
2181
|
+
const { imageData, mimeType } = await processImageToBase64(image, index, outputFormat);
|
|
2176
2182
|
content.push({
|
|
2177
2183
|
type: "image",
|
|
2178
2184
|
image: imageData,
|
|
@@ -2195,13 +2201,17 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
|
|
|
2195
2201
|
async function convertMultimodalToProviderFormat(text, images,
|
|
2196
2202
|
// The canonical entry shape (#309) rather than a fourth copy of it inline —
|
|
2197
2203
|
// which is what let the render knobs stop short of this function.
|
|
2198
|
-
pdfFiles, provider, model,
|
|
2204
|
+
pdfFiles, provider, model,
|
|
2205
|
+
// Bundled rather than two more positional params: a 7th would trip the
|
|
2206
|
+
// repo's `max-params` ceiling of 6.
|
|
2207
|
+
mediaOptions = {}) {
|
|
2208
|
+
const { audioFiles = [], outputFormat } = mediaOptions;
|
|
2199
2209
|
const content = [
|
|
2200
2210
|
{ type: "text", text },
|
|
2201
2211
|
];
|
|
2202
2212
|
// Add images if present
|
|
2203
2213
|
if (images.length > 0) {
|
|
2204
|
-
const imageContent = await convertSimpleImagesToProviderFormat("", images, provider, model);
|
|
2214
|
+
const imageContent = await convertSimpleImagesToProviderFormat("", images, provider, model, outputFormat);
|
|
2205
2215
|
if (Array.isArray(imageContent)) {
|
|
2206
2216
|
imageContent.forEach((item) => {
|
|
2207
2217
|
if (item.type !== "text") {
|
|
@@ -61,6 +61,9 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
|
|
|
61
61
|
scale?: number;
|
|
62
62
|
maxPages?: number;
|
|
63
63
|
} | undefined;
|
|
64
|
+
imageOptions: {
|
|
65
|
+
outputFormat?: import("../index.js").VisionImageOutputFormat;
|
|
66
|
+
} | undefined;
|
|
64
67
|
systemPrompt: string | undefined;
|
|
65
68
|
conversationHistory: import("../index.js").ChatMessage[] | undefined;
|
|
66
69
|
provider: string;
|
|
@@ -59,6 +59,7 @@ export function buildMultimodalOptions(options, providerName, modelName) {
|
|
|
59
59
|
},
|
|
60
60
|
csvOptions: options.csvOptions,
|
|
61
61
|
pdfOptions: options.pdfOptions,
|
|
62
|
+
imageOptions: options.imageOptions,
|
|
62
63
|
systemPrompt: options.systemPrompt,
|
|
63
64
|
conversationHistory: options.conversationMessages,
|
|
64
65
|
provider: providerName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.20.0",
|
|
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": {
|
|
@@ -242,6 +242,7 @@
|
|
|
242
242
|
"test:mcp-result-cache": "pnpm exec tsx test/continuous-test-suite-mcp-result-cache.ts",
|
|
243
243
|
"test:archive:security": "pnpm exec tsx test/continuous-test-suite-archive-security.ts",
|
|
244
244
|
"test:office:security": "pnpm exec tsx test/continuous-test-suite-office-security.ts",
|
|
245
|
+
"test:vision-image-format": "pnpm exec tsx test/continuous-test-suite-vision-image-format.ts",
|
|
245
246
|
"test:model-pool": "pnpm exec tsx test/continuous-test-suite-model-pool.ts",
|
|
246
247
|
"test:agents": "pnpm exec tsx test/continuous-test-suite-agents.ts",
|
|
247
248
|
"test:classifier-router": "pnpm exec tsx test/continuous-test-suite-classifier-router.ts",
|