@juspay/neurolink 12.14.21 → 12.15.1
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 +2 -6
- package/dist/browser/neurolink.min.js +397 -397
- package/dist/cli/commands/auth.js +6 -2
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +119 -39
- package/dist/cli/index.js +8 -18
- package/dist/cli/lifecycle.d.ts +8 -0
- package/dist/cli/lifecycle.js +90 -0
- package/dist/core/baseProvider.d.ts +6 -3
- package/dist/core/baseProvider.js +7 -3
- package/dist/providers/amazonBedrock/client.d.ts +7 -5
- package/dist/providers/amazonBedrock/client.js +125 -16
- package/dist/providers/cohere.d.ts +2 -2
- package/dist/providers/cohere.js +5 -1
- package/dist/providers/googleAiStudio/client.d.ts +3 -3
- package/dist/providers/googleAiStudio/client.js +6 -2
- package/dist/providers/googleVertex/client.d.ts +2 -2
- package/dist/providers/googleVertex/client.js +5 -1
- package/dist/providers/jina.d.ts +2 -2
- package/dist/providers/jina.js +5 -1
- package/dist/providers/litellm/client.d.ts +2 -2
- package/dist/providers/litellm/client.js +5 -1
- package/dist/providers/ollama/client.d.ts +2 -2
- package/dist/providers/ollama/client.js +5 -1
- package/dist/providers/openAI/client.d.ts +3 -3
- package/dist/providers/openAI/client.js +6 -2
- package/dist/providers/voyage.d.ts +2 -2
- package/dist/providers/voyage.js +5 -1
- package/dist/proxy/codexAccountUsage.js +18 -6
- package/dist/proxy/codexFallback.js +17 -10
- package/dist/proxy/codexUsage.js +3 -0
- package/dist/proxy/proxyAnalysis.js +14 -11
- package/dist/proxy/proxyTokenUsage.d.ts +7 -0
- package/dist/proxy/proxyTokenUsage.js +21 -0
- package/dist/proxy/proxyTracer.d.ts +2 -0
- package/dist/proxy/proxyTracer.js +21 -40
- package/dist/proxy/quotaHeaders.js +7 -3
- package/dist/rag/document/MDocument.d.ts +19 -1
- package/dist/rag/document/MDocument.js +30 -3
- package/dist/rag/document/imageLoader.d.ts +55 -0
- package/dist/rag/document/imageLoader.js +183 -0
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +2 -0
- package/dist/rag/pipeline/RAGPipeline.d.ts +54 -1
- package/dist/rag/pipeline/RAGPipeline.js +358 -1
- package/dist/rag/ragIntegration.js +146 -25
- package/dist/server/routes/claudeProxyRoutes.js +35 -1
- package/dist/server/routes/codexProxyRoutes.d.ts +5 -9
- package/dist/server/routes/codexProxyRoutes.js +106 -18
- package/dist/types/codex.d.ts +10 -0
- package/dist/types/providers.d.ts +21 -1
- package/dist/types/proxy.d.ts +24 -6
- package/dist/types/rag.d.ts +144 -0
- package/dist/types/rag.js +12 -1
- package/dist/utils/pricing.js +4 -2
- package/docs-site/static/search-index.json +1 -1
- package/package.json +2 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
1
2
|
import { createReadStream } from "node:fs";
|
|
2
3
|
import { isDeepStrictEqual } from "node:util";
|
|
3
4
|
import { isProxyAuxiliaryRequest } from "./proxyRequestKind.js";
|
|
@@ -411,8 +412,15 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
|
|
|
411
412
|
request.outputTokens !== null ||
|
|
412
413
|
request.cacheReadTokens !== null ||
|
|
413
414
|
request.cacheCreationTokens !== null) {
|
|
415
|
+
const billingUsage = proxyTokenUsage({
|
|
416
|
+
inputIncludesCachedTokens: request.inputIncludesCachedTokens,
|
|
417
|
+
inputTokens: request.inputTokens ?? 0,
|
|
418
|
+
outputTokens: request.outputTokens ?? 0,
|
|
419
|
+
cacheCreationTokens: request.cacheCreationTokens ?? 0,
|
|
420
|
+
cacheReadTokens: request.cacheReadTokens ?? 0,
|
|
421
|
+
});
|
|
414
422
|
requestsWithUsage += 1;
|
|
415
|
-
inputTokens +=
|
|
423
|
+
inputTokens += billingUsage.input;
|
|
416
424
|
outputTokens += request.outputTokens ?? 0;
|
|
417
425
|
cacheReadTokens += request.cacheReadTokens ?? 0;
|
|
418
426
|
cacheCreationTokens += request.cacheCreationTokens ?? 0;
|
|
@@ -422,16 +430,7 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
|
|
|
422
430
|
// "openai-compatible" resolves to the cross-provider table search in
|
|
423
431
|
// pricing.ts, which finds the model wherever it lives — a far better
|
|
424
432
|
// guess than assuming Anthropic and pricing a GPT model at $0.
|
|
425
|
-
const cost = calculateCost(request.provider ?? "openai-compatible", request.model,
|
|
426
|
-
input: request.inputTokens ?? 0,
|
|
427
|
-
output: request.outputTokens ?? 0,
|
|
428
|
-
total: (request.inputTokens ?? 0) +
|
|
429
|
-
(request.outputTokens ?? 0) +
|
|
430
|
-
(request.cacheCreationTokens ?? 0) +
|
|
431
|
-
(request.cacheReadTokens ?? 0),
|
|
432
|
-
cacheCreationTokens: request.cacheCreationTokens ?? 0,
|
|
433
|
-
cacheReadTokens: request.cacheReadTokens ?? 0,
|
|
434
|
-
});
|
|
433
|
+
const cost = calculateCost(request.provider ?? "openai-compatible", request.model, billingUsage);
|
|
435
434
|
// Ask the table directly rather than inferring from cost > 0: a real
|
|
436
435
|
// request with trivial usage can round to $0.000000 and is priced, not
|
|
437
436
|
// unpriced.
|
|
@@ -982,6 +981,10 @@ export async function analyzeProxyLogs(options) {
|
|
|
982
981
|
accountType: stringValue(record.accountType) ?? "unknown",
|
|
983
982
|
model: stringValue(record.model),
|
|
984
983
|
provider: stringValue(record.provider),
|
|
984
|
+
inputIncludesCachedTokens: typeof record.inputIncludesCachedTokens === "boolean"
|
|
985
|
+
? record.inputIncludesCachedTokens
|
|
986
|
+
: typeof record.path === "string" &&
|
|
987
|
+
record.path.includes("/backend-api/codex/"),
|
|
985
988
|
inputTokens: finiteNumber(record.inputTokens),
|
|
986
989
|
outputTokens: finiteNumber(record.outputTokens),
|
|
987
990
|
cacheReadTokens: finiteNumber(record.cacheReadTokens),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TokenUsage, UsageContext } from "../types/index.js";
|
|
2
|
+
/** Convert proxy wire usage into disjoint billing buckets. Codex input already
|
|
3
|
+
* includes cache tokens; reasoning is included in output for both providers.
|
|
4
|
+
* OpenAI's documented formula subtracts both cached_tokens and cache_write_tokens:
|
|
5
|
+
* https://developers.openai.com/api/docs/guides/prompt-caching#monitor-cache-performance
|
|
6
|
+
*/
|
|
7
|
+
export declare function proxyTokenUsage(usage: UsageContext): TokenUsage;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Convert proxy wire usage into disjoint billing buckets. Codex input already
|
|
2
|
+
* includes cache tokens; reasoning is included in output for both providers.
|
|
3
|
+
* OpenAI's documented formula subtracts both cached_tokens and cache_write_tokens:
|
|
4
|
+
* https://developers.openai.com/api/docs/guides/prompt-caching#monitor-cache-performance
|
|
5
|
+
*/
|
|
6
|
+
export function proxyTokenUsage(usage) {
|
|
7
|
+
const cached = usage.cacheReadTokens + usage.cacheCreationTokens;
|
|
8
|
+
const input = usage.inputIncludesCachedTokens
|
|
9
|
+
? Math.max(0, usage.inputTokens - cached)
|
|
10
|
+
: usage.inputTokens;
|
|
11
|
+
return {
|
|
12
|
+
input,
|
|
13
|
+
output: usage.outputTokens,
|
|
14
|
+
total: usage.inputIncludesCachedTokens
|
|
15
|
+
? usage.inputTokens + usage.outputTokens
|
|
16
|
+
: input + cached + usage.outputTokens,
|
|
17
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
18
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
19
|
+
reasoning: usage.reasoningTokens,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -57,6 +57,8 @@ declare class ProxyTracer {
|
|
|
57
57
|
startStream(): Span;
|
|
58
58
|
/** Record account selection outcome on the root span. */
|
|
59
59
|
setAccountSelection(ctx: AccountSelectionContext): void;
|
|
60
|
+
/** Attribute fallback metrics to the account that actually served them. */
|
|
61
|
+
setServedAccount(account: string, accountType: string): void;
|
|
60
62
|
/** Record token usage and cost on the root span. */
|
|
61
63
|
setUsage(ctx: UsageContext): void;
|
|
62
64
|
/**
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { SpanStatusCode, SpanKind, ROOT_CONTEXT, isSpanContextValid, propagation, context, metrics, trace, } from "@opentelemetry/api";
|
|
17
17
|
import { getTracer, setLangfuseContext, } from "../services/server/ai/observability/instrumentation.js";
|
|
18
18
|
import { OtelBridge } from "../observability/otelBridge.js";
|
|
19
|
+
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
19
20
|
import { calculateCost } from "../utils/pricing.js";
|
|
20
21
|
import { TelemetryService } from "../telemetry/telemetryService.js";
|
|
21
22
|
import { logger } from "../utils/logger.js";
|
|
@@ -360,17 +361,22 @@ class ProxyTracer {
|
|
|
360
361
|
// Non-fatal
|
|
361
362
|
});
|
|
362
363
|
}
|
|
364
|
+
/** Attribute fallback metrics to the account that actually served them. */
|
|
365
|
+
setServedAccount(account, accountType) {
|
|
366
|
+
this.accountEmail = account;
|
|
367
|
+
this.rootSpan.setAttributes({
|
|
368
|
+
"proxy.account.served": account,
|
|
369
|
+
"proxy.account.served_type": accountType,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
363
372
|
/** Record token usage and cost on the root span. */
|
|
364
373
|
setUsage(ctx) {
|
|
365
374
|
this.usage = ctx;
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
ctx.cacheReadTokens +
|
|
370
|
-
(ctx.reasoningTokens ?? 0);
|
|
371
|
-
// NeuroLink-format token attributes (from SpanAttributes)
|
|
375
|
+
const billingUsage = proxyTokenUsage(ctx);
|
|
376
|
+
const totalTokens = billingUsage.total;
|
|
377
|
+
// Custom token buckets are disjoint; standard GenAI input includes cache.
|
|
372
378
|
this.rootSpan.setAttributes({
|
|
373
|
-
"ai.tokens.input":
|
|
379
|
+
"ai.tokens.input": billingUsage.input,
|
|
374
380
|
"ai.tokens.output": ctx.outputTokens,
|
|
375
381
|
"ai.tokens.total": totalTokens,
|
|
376
382
|
"ai.tokens.cache_creation": ctx.cacheCreationTokens,
|
|
@@ -381,18 +387,12 @@ class ProxyTracer {
|
|
|
381
387
|
}
|
|
382
388
|
// GenAI semantic convention attributes (for Langfuse compatibility)
|
|
383
389
|
this.rootSpan.setAttributes({
|
|
384
|
-
"gen_ai.usage.input_tokens": ctx.
|
|
390
|
+
"gen_ai.usage.input_tokens": totalTokens - ctx.outputTokens,
|
|
385
391
|
"gen_ai.usage.output_tokens": ctx.outputTokens,
|
|
386
392
|
"gen_ai.usage.total_tokens": totalTokens,
|
|
387
393
|
});
|
|
388
394
|
// Cost calculation via pricing.ts
|
|
389
|
-
const cost = calculateCost(this.billingProvider, this.model,
|
|
390
|
-
input: ctx.inputTokens,
|
|
391
|
-
output: ctx.outputTokens,
|
|
392
|
-
total: totalTokens,
|
|
393
|
-
cacheCreationTokens: ctx.cacheCreationTokens,
|
|
394
|
-
cacheReadTokens: ctx.cacheReadTokens,
|
|
395
|
-
});
|
|
395
|
+
const cost = calculateCost(this.billingProvider, this.model, billingUsage);
|
|
396
396
|
if (cost > 0) {
|
|
397
397
|
this.rootSpan.setAttributes({
|
|
398
398
|
"ai.cost.total": cost,
|
|
@@ -665,7 +665,7 @@ class ProxyTracer {
|
|
|
665
665
|
model: this.model,
|
|
666
666
|
account: this.accountEmail ?? "unknown",
|
|
667
667
|
};
|
|
668
|
-
m.tokensInput.add(this.usage.
|
|
668
|
+
m.tokensInput.add(proxyTokenUsage(this.usage).input, tokenLabels);
|
|
669
669
|
m.tokensOutput.add(this.usage.outputTokens, tokenLabels);
|
|
670
670
|
m.tokensCacheRead.add(this.usage.cacheReadTokens, tokenLabels);
|
|
671
671
|
m.tokensCacheCreation.add(this.usage.cacheCreationTokens, tokenLabels);
|
|
@@ -673,18 +673,8 @@ class ProxyTracer {
|
|
|
673
673
|
m.tokensReasoning.add(this.usage.reasoningTokens, tokenLabels);
|
|
674
674
|
}
|
|
675
675
|
// Cost
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
this.usage.cacheCreationTokens +
|
|
679
|
-
this.usage.cacheReadTokens +
|
|
680
|
-
(this.usage.reasoningTokens ?? 0);
|
|
681
|
-
const cost = calculateCost(this.billingProvider, this.model, {
|
|
682
|
-
input: this.usage.inputTokens,
|
|
683
|
-
output: this.usage.outputTokens,
|
|
684
|
-
total: totalTokens,
|
|
685
|
-
cacheCreationTokens: this.usage.cacheCreationTokens,
|
|
686
|
-
cacheReadTokens: this.usage.cacheReadTokens,
|
|
687
|
-
});
|
|
676
|
+
const billingUsage = proxyTokenUsage(this.usage);
|
|
677
|
+
const cost = calculateCost(this.billingProvider, this.model, billingUsage);
|
|
688
678
|
if (cost > 0) {
|
|
689
679
|
m.costTotal.add(cost, tokenLabels);
|
|
690
680
|
}
|
|
@@ -711,19 +701,10 @@ class ProxyTracer {
|
|
|
711
701
|
if (!this.usage) {
|
|
712
702
|
return;
|
|
713
703
|
}
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
this.usage.cacheCreationTokens +
|
|
717
|
-
this.usage.cacheReadTokens +
|
|
718
|
-
(this.usage.reasoningTokens ?? 0);
|
|
704
|
+
const billingUsage = proxyTokenUsage(this.usage);
|
|
705
|
+
const totalTokens = billingUsage.total;
|
|
719
706
|
const durationMs = Date.now() - this.startTime;
|
|
720
|
-
const cost = calculateCost(this.billingProvider, this.model,
|
|
721
|
-
input: this.usage.inputTokens,
|
|
722
|
-
output: this.usage.outputTokens,
|
|
723
|
-
total: totalTokens,
|
|
724
|
-
cacheCreationTokens: this.usage.cacheCreationTokens,
|
|
725
|
-
cacheReadTokens: this.usage.cacheReadTokens,
|
|
726
|
-
});
|
|
707
|
+
const cost = calculateCost(this.billingProvider, this.model, billingUsage);
|
|
727
708
|
TelemetryService.getInstance().recordAIRequest(this.billingProvider, this.model, totalTokens, durationMs, cost > 0 ? cost : undefined);
|
|
728
709
|
}
|
|
729
710
|
// -------------------------------------------------------------------------
|
|
@@ -133,10 +133,14 @@ export function buildQuotaResponseHeaders(context, now = Date.now()) {
|
|
|
133
133
|
}
|
|
134
134
|
const quota = context.quota;
|
|
135
135
|
if (quota && context.source !== "none") {
|
|
136
|
-
set("x-neurolink-quota-session-left-pct",
|
|
136
|
+
set("x-neurolink-quota-session-left-pct", quota.sessionStatus === "unknown"
|
|
137
|
+
? undefined
|
|
138
|
+
: utilizationToLeftPct(quota.sessionUsed));
|
|
137
139
|
set("x-neurolink-quota-session-status", quota.sessionStatus);
|
|
138
140
|
set("x-neurolink-quota-session-resets-in", secondsUntil(quota.sessionResetAt, now));
|
|
139
|
-
set("x-neurolink-quota-weekly-left-pct",
|
|
141
|
+
set("x-neurolink-quota-weekly-left-pct", quota.weeklyStatus === "unknown"
|
|
142
|
+
? undefined
|
|
143
|
+
: utilizationToLeftPct(quota.weeklyUsed));
|
|
140
144
|
set("x-neurolink-quota-weekly-status", quota.weeklyStatus);
|
|
141
145
|
set("x-neurolink-quota-weekly-resets-in", secondsUntil(quota.weeklyResetAt, now));
|
|
142
146
|
set("x-neurolink-quota-unified-status", quota.unifiedStatus);
|
|
@@ -173,7 +177,7 @@ export function summarizePoolHeadroom(entries, now = Date.now()) {
|
|
|
173
177
|
continue;
|
|
174
178
|
}
|
|
175
179
|
available += 1;
|
|
176
|
-
if (entry.quota) {
|
|
180
|
+
if (entry.quota && entry.quota.sessionStatus !== "unknown") {
|
|
177
181
|
const left = utilizationToLeftPct(entry.quota.sessionUsed);
|
|
178
182
|
if (bestSessionLeftPct === undefined || left > bestSessionLeftPct) {
|
|
179
183
|
bestSessionLeftPct = left;
|
|
@@ -99,9 +99,27 @@ export declare class MDocument {
|
|
|
99
99
|
* Generate embeddings for all chunks
|
|
100
100
|
* @param provider - Embedding provider name
|
|
101
101
|
* @param modelName - Embedding model name
|
|
102
|
+
* @param imageData - Optional array of image data (Buffer or base64) aligned with chunks,
|
|
103
|
+
* enabling multi-modal embeddings for providers that support them
|
|
104
|
+
* @param mimeType - MIME type of the images (default: "image/png"). Pass an
|
|
105
|
+
* array to declare a per-image MIME type aligned with
|
|
106
|
+
* `imageData`; a plain string applies to every image.
|
|
102
107
|
* @returns This MDocument instance (for chaining)
|
|
103
108
|
*/
|
|
104
|
-
embed(provider?: string, modelName?: string): Promise<MDocument>;
|
|
109
|
+
embed(provider?: string, modelName?: string, imageData?: Array<Buffer | string>, mimeType?: string | string[]): Promise<MDocument>;
|
|
110
|
+
/**
|
|
111
|
+
* Generate multi-modal embeddings for all chunks using text + optional image data.
|
|
112
|
+
* Uses a provider that supports multi-modal embeddings (e.g. Bedrock Titan Image).
|
|
113
|
+
*
|
|
114
|
+
* @param provider - Embedding provider name
|
|
115
|
+
* @param modelName - Embedding model name
|
|
116
|
+
* @param imageData - Optional array of image data (Buffer or base64) aligned with chunks
|
|
117
|
+
* @param mimeType - MIME type of the images (default: "image/png"). Pass an
|
|
118
|
+
* array to declare a per-image MIME type aligned with
|
|
119
|
+
* `imageData`; a plain string applies to every image.
|
|
120
|
+
* @returns This MDocument instance (for chaining)
|
|
121
|
+
*/
|
|
122
|
+
embedMultiModal(provider?: string, modelName?: string, imageData?: Array<Buffer | string>, mimeType?: string | string[]): Promise<MDocument>;
|
|
105
123
|
/**
|
|
106
124
|
* Get document ID
|
|
107
125
|
*/
|
|
@@ -194,9 +194,14 @@ export class MDocument {
|
|
|
194
194
|
* Generate embeddings for all chunks
|
|
195
195
|
* @param provider - Embedding provider name
|
|
196
196
|
* @param modelName - Embedding model name
|
|
197
|
+
* @param imageData - Optional array of image data (Buffer or base64) aligned with chunks,
|
|
198
|
+
* enabling multi-modal embeddings for providers that support them
|
|
199
|
+
* @param mimeType - MIME type of the images (default: "image/png"). Pass an
|
|
200
|
+
* array to declare a per-image MIME type aligned with
|
|
201
|
+
* `imageData`; a plain string applies to every image.
|
|
197
202
|
* @returns This MDocument instance (for chaining)
|
|
198
203
|
*/
|
|
199
|
-
async embed(provider = "openai", modelName = "text-embedding-3-small") {
|
|
204
|
+
async embed(provider = "openai", modelName = "text-embedding-3-small", imageData, mimeType = "image/png") {
|
|
200
205
|
if (this.state.chunks.length === 0) {
|
|
201
206
|
logger.warn("[MDocument] No chunks to embed. Call chunk() first.");
|
|
202
207
|
return this;
|
|
@@ -208,14 +213,21 @@ export class MDocument {
|
|
|
208
213
|
chunkCount: this.state.chunks.length,
|
|
209
214
|
provider,
|
|
210
215
|
model: modelName,
|
|
216
|
+
hasImages: imageData !== undefined && imageData.length > 0,
|
|
211
217
|
});
|
|
212
218
|
const embeddingProvider = await ProviderFactory.createProvider(provider, modelName);
|
|
213
219
|
if (typeof embeddingProvider.embed !== "function") {
|
|
214
220
|
throw new Error(`Provider ${provider} does not support embeddings`);
|
|
215
221
|
}
|
|
216
222
|
this.state.embeddings = [];
|
|
217
|
-
for (
|
|
218
|
-
const
|
|
223
|
+
for (let i = 0; i < this.state.chunks.length; i++) {
|
|
224
|
+
const chunk = this.state.chunks[i];
|
|
225
|
+
const image = imageData?.[i];
|
|
226
|
+
const imageMimeType = typeof mimeType === "string" ? mimeType : (mimeType[i] ?? "image/png");
|
|
227
|
+
const input = image
|
|
228
|
+
? { text: chunk.text, image, mimeType: imageMimeType }
|
|
229
|
+
: chunk.text;
|
|
230
|
+
const embedding = await embeddingProvider.embed(input);
|
|
219
231
|
this.state.embeddings.push(embedding);
|
|
220
232
|
chunk.embedding = embedding;
|
|
221
233
|
}
|
|
@@ -227,6 +239,21 @@ export class MDocument {
|
|
|
227
239
|
});
|
|
228
240
|
return this;
|
|
229
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Generate multi-modal embeddings for all chunks using text + optional image data.
|
|
244
|
+
* Uses a provider that supports multi-modal embeddings (e.g. Bedrock Titan Image).
|
|
245
|
+
*
|
|
246
|
+
* @param provider - Embedding provider name
|
|
247
|
+
* @param modelName - Embedding model name
|
|
248
|
+
* @param imageData - Optional array of image data (Buffer or base64) aligned with chunks
|
|
249
|
+
* @param mimeType - MIME type of the images (default: "image/png"). Pass an
|
|
250
|
+
* array to declare a per-image MIME type aligned with
|
|
251
|
+
* `imageData`; a plain string applies to every image.
|
|
252
|
+
* @returns This MDocument instance (for chaining)
|
|
253
|
+
*/
|
|
254
|
+
async embedMultiModal(provider = "bedrock", modelName = "amazon.titan-embed-image-v1", imageData, mimeType = "image/png") {
|
|
255
|
+
return this.embed(provider, modelName, imageData, mimeType);
|
|
256
|
+
}
|
|
230
257
|
// ============================================================================
|
|
231
258
|
// Accessor Methods
|
|
232
259
|
// ============================================================================
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Loader for Multi-Modal RAG
|
|
3
|
+
*
|
|
4
|
+
* Loads image files (from disk or URLs) into ImageDocument objects
|
|
5
|
+
* that can be embedded and stored in a vector store alongside text chunks.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { ImageLoader } from './imageLoader.js';
|
|
10
|
+
*
|
|
11
|
+
* const loader = new ImageLoader();
|
|
12
|
+
*
|
|
13
|
+
* // Load from file path
|
|
14
|
+
* const doc = await loader.load('./photo.jpg');
|
|
15
|
+
*
|
|
16
|
+
* // Load from URL
|
|
17
|
+
* const doc2 = await loader.load('https://example.com/image.png');
|
|
18
|
+
*
|
|
19
|
+
* // Load from buffer
|
|
20
|
+
* const doc3 = loader.loadFromBuffer(imageBuffer, 'image/jpeg');
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import type { ImageDocument, ImageLoaderOptions } from "../../types/index.js";
|
|
24
|
+
/**
|
|
25
|
+
* ImageLoader for multi-modal RAG.
|
|
26
|
+
* Loads images from file paths, URLs, or buffers into ImageDocument objects.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ImageLoader {
|
|
29
|
+
private options;
|
|
30
|
+
constructor(options?: ImageLoaderOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Load an image from a file path or URL.
|
|
33
|
+
*/
|
|
34
|
+
load(source: string, options?: Partial<ImageLoaderOptions>): Promise<ImageDocument>;
|
|
35
|
+
/**
|
|
36
|
+
* Load an image from a local file path.
|
|
37
|
+
*/
|
|
38
|
+
private loadFromPath;
|
|
39
|
+
/**
|
|
40
|
+
* Load an image from a URL.
|
|
41
|
+
*/
|
|
42
|
+
private loadFromURL;
|
|
43
|
+
/**
|
|
44
|
+
* Load an image directly from a Buffer.
|
|
45
|
+
*/
|
|
46
|
+
loadFromBuffer(buffer: Buffer, mimeType: string, source?: string, options?: Partial<ImageLoaderOptions>): ImageDocument;
|
|
47
|
+
/**
|
|
48
|
+
* Check if a file path or extension is a supported image format.
|
|
49
|
+
*/
|
|
50
|
+
static isImageFile(source: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Get the MIME type for a file extension.
|
|
53
|
+
*/
|
|
54
|
+
static getMimeType(filePath: string): string | undefined;
|
|
55
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Loader for Multi-Modal RAG
|
|
3
|
+
*
|
|
4
|
+
* Loads image files (from disk or URLs) into ImageDocument objects
|
|
5
|
+
* that can be embedded and stored in a vector store alongside text chunks.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { ImageLoader } from './imageLoader.js';
|
|
10
|
+
*
|
|
11
|
+
* const loader = new ImageLoader();
|
|
12
|
+
*
|
|
13
|
+
* // Load from file path
|
|
14
|
+
* const doc = await loader.load('./photo.jpg');
|
|
15
|
+
*
|
|
16
|
+
* // Load from URL
|
|
17
|
+
* const doc2 = await loader.load('https://example.com/image.png');
|
|
18
|
+
*
|
|
19
|
+
* // Load from buffer
|
|
20
|
+
* const doc3 = loader.loadFromBuffer(imageBuffer, 'image/jpeg');
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
24
|
+
import { extname } from "path";
|
|
25
|
+
import { logger } from "../../utils/logger.js";
|
|
26
|
+
import { safeDownload } from "../../utils/safeFetch.js";
|
|
27
|
+
import { redactUrlForError } from "../../utils/logSanitize.js";
|
|
28
|
+
import { ImageProcessor, VALID_IMAGE_EXTENSIONS, } from "../../utils/imageProcessor.js";
|
|
29
|
+
import { EXTENSION_MIME_MAP } from "../../processors/config/index.js";
|
|
30
|
+
/**
|
|
31
|
+
* Generate a text representation from the image source.
|
|
32
|
+
*/
|
|
33
|
+
function generateTextDescription(source) {
|
|
34
|
+
// Drop any query string or fragment before deriving the name. On a presigned
|
|
35
|
+
// URL the signature lives in the query, and the query is part of the final
|
|
36
|
+
// slash-separated segment — so `.pop()` alone keeps it, and this caption is
|
|
37
|
+
// not a transient log line: it becomes embedded, BM25-indexed chunk text that
|
|
38
|
+
// persists in the vector store and is read back into model context.
|
|
39
|
+
const withoutQueryOrFragment = source.split(/[?#]/)[0];
|
|
40
|
+
const filename = withoutQueryOrFragment.split("/").pop() ?? source;
|
|
41
|
+
// Remove extension and clean up
|
|
42
|
+
const name = filename
|
|
43
|
+
.replace(/\.[^.]+$/, "")
|
|
44
|
+
.replace(/[-_]/g, " ")
|
|
45
|
+
.replace(/\s+/g, " ")
|
|
46
|
+
.trim();
|
|
47
|
+
return `Image: ${name}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* ImageLoader for multi-modal RAG.
|
|
51
|
+
* Loads images from file paths, URLs, or buffers into ImageDocument objects.
|
|
52
|
+
*/
|
|
53
|
+
export class ImageLoader {
|
|
54
|
+
options;
|
|
55
|
+
constructor(options) {
|
|
56
|
+
this.options = {
|
|
57
|
+
metadata: options?.metadata ?? {},
|
|
58
|
+
maxImageSize: options?.maxImageSize ?? 10 * 1024 * 1024, // 10MB
|
|
59
|
+
fetchTimeout: options?.fetchTimeout ?? 30_000,
|
|
60
|
+
headers: options?.headers ?? {},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Load an image from a file path or URL.
|
|
65
|
+
*/
|
|
66
|
+
async load(source, options) {
|
|
67
|
+
const opts = { ...this.options, ...options };
|
|
68
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
69
|
+
return this.loadFromURL(source, opts);
|
|
70
|
+
}
|
|
71
|
+
return this.loadFromPath(source, opts);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load an image from a local file path.
|
|
75
|
+
*/
|
|
76
|
+
async loadFromPath(filePath, options) {
|
|
77
|
+
if (!existsSync(filePath)) {
|
|
78
|
+
throw new Error(`Image file not found: ${filePath}`);
|
|
79
|
+
}
|
|
80
|
+
const { size } = statSync(filePath);
|
|
81
|
+
if (size > options.maxImageSize) {
|
|
82
|
+
throw new Error(`Image size (${size} bytes) exceeds maximum (${options.maxImageSize} bytes)`);
|
|
83
|
+
}
|
|
84
|
+
const buffer = readFileSync(filePath);
|
|
85
|
+
const ext = extname(filePath).toLowerCase();
|
|
86
|
+
const mimeType = EXTENSION_MIME_MAP[ext] ?? ImageProcessor.detectImageType(buffer);
|
|
87
|
+
const dimensions = ImageProcessor.getImageDimensions(buffer);
|
|
88
|
+
logger.debug("[ImageLoader] Loaded image from path", {
|
|
89
|
+
path: filePath,
|
|
90
|
+
size: buffer.byteLength,
|
|
91
|
+
mimeType,
|
|
92
|
+
dimensions,
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
id: `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
96
|
+
text: generateTextDescription(filePath),
|
|
97
|
+
image: buffer,
|
|
98
|
+
mimeType,
|
|
99
|
+
metadata: {
|
|
100
|
+
source: filePath,
|
|
101
|
+
width: dimensions?.width,
|
|
102
|
+
height: dimensions?.height,
|
|
103
|
+
format: ext.replace(".", ""),
|
|
104
|
+
size: buffer.byteLength,
|
|
105
|
+
...options.metadata,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Load an image from a URL.
|
|
111
|
+
*/
|
|
112
|
+
async loadFromURL(url, options) {
|
|
113
|
+
const buffer = await safeDownload(url, {
|
|
114
|
+
maxBytes: options.maxImageSize,
|
|
115
|
+
label: "RAG image",
|
|
116
|
+
timeoutMs: options.fetchTimeout,
|
|
117
|
+
});
|
|
118
|
+
const mimeType = ImageProcessor.detectImageType(buffer);
|
|
119
|
+
const dimensions = ImageProcessor.getImageDimensions(buffer);
|
|
120
|
+
logger.debug("[ImageLoader] Loaded image from URL", {
|
|
121
|
+
url: redactUrlForError(url),
|
|
122
|
+
size: buffer.byteLength,
|
|
123
|
+
mimeType,
|
|
124
|
+
dimensions,
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
id: `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
128
|
+
text: generateTextDescription(url),
|
|
129
|
+
image: buffer,
|
|
130
|
+
mimeType,
|
|
131
|
+
metadata: {
|
|
132
|
+
// Redacted for the same reason as the caption above: `source` is
|
|
133
|
+
// persisted into the vector store and returned verbatim to the model
|
|
134
|
+
// as the RAG tool's citation, so a signed URL stored here outlives the
|
|
135
|
+
// request that fetched it.
|
|
136
|
+
source: redactUrlForError(url),
|
|
137
|
+
width: dimensions?.width,
|
|
138
|
+
height: dimensions?.height,
|
|
139
|
+
format: mimeType.split("/").at(1),
|
|
140
|
+
size: buffer.byteLength,
|
|
141
|
+
...options.metadata,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Load an image directly from a Buffer.
|
|
147
|
+
*/
|
|
148
|
+
loadFromBuffer(buffer, mimeType, source, options) {
|
|
149
|
+
const opts = { ...this.options, ...options };
|
|
150
|
+
if (buffer.byteLength > opts.maxImageSize) {
|
|
151
|
+
throw new Error(`Image size (${buffer.byteLength} bytes) exceeds maximum (${opts.maxImageSize} bytes)`);
|
|
152
|
+
}
|
|
153
|
+
const dimensions = ImageProcessor.getImageDimensions(buffer);
|
|
154
|
+
return {
|
|
155
|
+
id: `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
156
|
+
text: generateTextDescription(source ?? "inline-image"),
|
|
157
|
+
image: buffer,
|
|
158
|
+
mimeType,
|
|
159
|
+
metadata: {
|
|
160
|
+
source: source ?? "inline",
|
|
161
|
+
width: dimensions?.width,
|
|
162
|
+
height: dimensions?.height,
|
|
163
|
+
format: mimeType.split("/").at(1),
|
|
164
|
+
size: buffer.byteLength,
|
|
165
|
+
...opts.metadata,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if a file path or extension is a supported image format.
|
|
171
|
+
*/
|
|
172
|
+
static isImageFile(source) {
|
|
173
|
+
const ext = extname(source).toLowerCase().replace(/^\./, "");
|
|
174
|
+
return VALID_IMAGE_EXTENSIONS.includes(ext);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get the MIME type for a file extension.
|
|
178
|
+
*/
|
|
179
|
+
static getMimeType(filePath) {
|
|
180
|
+
const ext = extname(filePath).toLowerCase();
|
|
181
|
+
return EXTENSION_MIME_MAP[ext];
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dist/rag/index.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export { ChunkerRegistry as ChunkerRegistryV2, chunkerRegistry, getAvailableChun
|
|
|
48
48
|
export * from "./chunkers/index.js";
|
|
49
49
|
export { CharacterChunker, ChunkerRegistry, chunkText, HTMLChunker, JSONChunker, LaTeXChunker, MarkdownChunker, RecursiveChunker, SemanticChunker, SentenceChunker, TokenChunker, } from "./chunking/index.js";
|
|
50
50
|
export { CSVLoader, HTMLLoader, JSONLoader, loadDocument, loadDocuments, MarkdownLoader, MDocument, PDFLoader, TextLoader, WebLoader, } from "./document/index.js";
|
|
51
|
+
export { ImageLoader } from "./document/imageLoader.js";
|
|
51
52
|
export * from "./errors/index.js";
|
|
52
53
|
export { GraphRAG } from "./graphRag/index.js";
|
|
53
54
|
export { createMetadataExtractor, extractMetadata, getAvailableExtractors, getAvailableExtractorTypes, getExtractor, getExtractorDefaultConfig, getExtractorMetadata, getRegisteredExtractorMetadata, LLMMetadataExtractor, MetadataExtractorFactory, MetadataExtractorRegistry, metadataExtractorFactory, metadataExtractorRegistry, } from "./metadata/index.js";
|
package/dist/rag/index.js
CHANGED
|
@@ -52,6 +52,8 @@ export * from "./chunkers/index.js";
|
|
|
52
52
|
export { CharacterChunker, ChunkerRegistry, chunkText, HTMLChunker, JSONChunker, LaTeXChunker, MarkdownChunker, RecursiveChunker, SemanticChunker, SentenceChunker, TokenChunker, } from "./chunking/index.js";
|
|
53
53
|
// Document Processing
|
|
54
54
|
export { CSVLoader, HTMLLoader, JSONLoader, loadDocument, loadDocuments, MarkdownLoader, MDocument, PDFLoader, TextLoader, WebLoader, } from "./document/index.js";
|
|
55
|
+
// Image Loader for multi-modal RAG
|
|
56
|
+
export { ImageLoader } from "./document/imageLoader.js";
|
|
55
57
|
// Error handling
|
|
56
58
|
export * from "./errors/index.js";
|
|
57
59
|
// Graph RAG
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* console.log(response.answer, response.sources);
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
-
import type { RAGPipelineConfig, IngestOptions, QueryOptions, RAGResponse, PipelineStats } from "../../types/index.js";
|
|
28
|
+
import type { RAGPipelineConfig, IngestOptions, QueryOptions, RAGResponse, PipelineStats, MultiModalRAGConfig, MultiModalQuery, MultiModalSearchResult } from "../../types/index.js";
|
|
29
29
|
import { MDocument } from "../document/MDocument.js";
|
|
30
30
|
export declare class RAGPipeline {
|
|
31
31
|
private id;
|
|
@@ -34,10 +34,21 @@ export declare class RAGPipeline {
|
|
|
34
34
|
private bm25Index;
|
|
35
35
|
private graphRAG;
|
|
36
36
|
private embeddingProvider?;
|
|
37
|
+
private multiModalEmbeddingProvider?;
|
|
37
38
|
private generationProvider?;
|
|
39
|
+
/**
|
|
40
|
+
* Vision provider for image captioning, when `multiModal.captionProvider` /
|
|
41
|
+
* `captionModel` name one. Undefined means captioning uses
|
|
42
|
+
* {@link generationProvider} — captioning has always run on that provider,
|
|
43
|
+
* and these two fields were declared but read nowhere, so a caller who set
|
|
44
|
+
* them got the pipeline's generation model with no indication otherwise.
|
|
45
|
+
*/
|
|
46
|
+
private captionProvider?;
|
|
38
47
|
private hybridSearch?;
|
|
39
48
|
private documents;
|
|
40
49
|
private allChunks;
|
|
50
|
+
private multiModalConfig?;
|
|
51
|
+
private imageLoader;
|
|
41
52
|
constructor(config: RAGPipelineConfig);
|
|
42
53
|
/**
|
|
43
54
|
* Initialize the pipeline (lazy loading of providers)
|
|
@@ -73,6 +84,47 @@ export declare class RAGPipeline {
|
|
|
73
84
|
* Clear all indexed data
|
|
74
85
|
*/
|
|
75
86
|
clear(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Ingest images into the pipeline for multi-modal RAG.
|
|
89
|
+
* Loads images, generates embeddings via the configured multi-modal provider,
|
|
90
|
+
* and stores them alongside text chunks in the vector store.
|
|
91
|
+
*
|
|
92
|
+
* @param sources - Array of image file paths, URLs, or Buffer objects
|
|
93
|
+
* @param options - Ingestion options
|
|
94
|
+
*/
|
|
95
|
+
ingestImages(sources: Array<string | Buffer>, options?: IngestOptions): Promise<{
|
|
96
|
+
imagesProcessed: number;
|
|
97
|
+
chunksCreated: number;
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Query the pipeline with multi-modal input (text, image, or both).
|
|
101
|
+
*
|
|
102
|
+
* @param query - Text, image, or combined query
|
|
103
|
+
* @param options - Query options
|
|
104
|
+
* @returns Array of multi-modal search results
|
|
105
|
+
*/
|
|
106
|
+
queryMultiModal(query: MultiModalQuery, options?: QueryOptions): Promise<MultiModalSearchResult[]>;
|
|
107
|
+
/**
|
|
108
|
+
* Get multi-modal pipeline statistics
|
|
109
|
+
*/
|
|
110
|
+
getMultiModalStats(): {
|
|
111
|
+
totalImages: number;
|
|
112
|
+
totalTextChunks: number;
|
|
113
|
+
multiModalEnabled: boolean;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Generate a multi-modal embedding for text, image, or both.
|
|
117
|
+
* Uses the configured multi-modal embedding provider (e.g. Bedrock Titan Image).
|
|
118
|
+
*/
|
|
119
|
+
private generateMultiModalEmbedding;
|
|
120
|
+
/**
|
|
121
|
+
* Generate a caption for an image using the generation provider's vision model.
|
|
122
|
+
*/
|
|
123
|
+
private generateImageCaption;
|
|
124
|
+
/**
|
|
125
|
+
* Diversify multi-modal results to ensure a mix of text and image results.
|
|
126
|
+
*/
|
|
127
|
+
private diversifyMultiModalResults;
|
|
76
128
|
/**
|
|
77
129
|
* Ensure pipeline is initialized
|
|
78
130
|
*/
|
|
@@ -102,4 +154,5 @@ export declare function createRAGPipeline(options: {
|
|
|
102
154
|
generationModel?: string;
|
|
103
155
|
enableHybrid?: boolean;
|
|
104
156
|
enableGraph?: boolean;
|
|
157
|
+
multiModal?: MultiModalRAGConfig;
|
|
105
158
|
}): RAGPipeline;
|