@juspay/neurolink 9.15.0 → 9.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/adapters/video/videoAnalyzer.js +10 -8
- package/dist/cli/commands/setup-anthropic.js +1 -14
- package/dist/cli/commands/setup-azure.js +1 -12
- package/dist/cli/commands/setup-bedrock.js +1 -9
- package/dist/cli/commands/setup-google-ai.js +1 -12
- package/dist/cli/commands/setup-openai.js +1 -14
- package/dist/cli/commands/workflow.d.ts +27 -0
- package/dist/cli/commands/workflow.js +216 -0
- package/dist/cli/factories/commandFactory.js +79 -20
- package/dist/cli/index.js +0 -1
- package/dist/cli/parser.js +4 -1
- package/dist/cli/utils/maskCredential.d.ts +11 -0
- package/dist/cli/utils/maskCredential.js +23 -0
- package/dist/constants/contextWindows.js +107 -16
- package/dist/constants/enums.d.ts +99 -15
- package/dist/constants/enums.js +152 -22
- package/dist/context/budgetChecker.js +1 -1
- package/dist/context/contextCompactor.js +31 -4
- package/dist/context/emergencyTruncation.d.ts +21 -0
- package/dist/context/emergencyTruncation.js +88 -0
- package/dist/context/errorDetection.d.ts +16 -0
- package/dist/context/errorDetection.js +48 -1
- package/dist/context/errors.d.ts +19 -0
- package/dist/context/errors.js +21 -0
- package/dist/context/stages/slidingWindowTruncator.d.ts +6 -0
- package/dist/context/stages/slidingWindowTruncator.js +159 -24
- package/dist/core/baseProvider.js +306 -200
- package/dist/core/conversationMemoryManager.js +104 -61
- package/dist/core/evaluationProviders.js +16 -33
- package/dist/core/factory.js +237 -164
- package/dist/core/modules/GenerationHandler.js +175 -116
- package/dist/core/modules/MessageBuilder.js +222 -170
- package/dist/core/modules/StreamHandler.d.ts +1 -0
- package/dist/core/modules/StreamHandler.js +95 -27
- package/dist/core/modules/TelemetryHandler.d.ts +10 -1
- package/dist/core/modules/TelemetryHandler.js +25 -7
- package/dist/core/modules/ToolsManager.js +115 -191
- package/dist/core/redisConversationMemoryManager.js +418 -282
- package/dist/factories/providerRegistry.d.ts +5 -0
- package/dist/factories/providerRegistry.js +20 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/lib/adapters/video/videoAnalyzer.js +10 -8
- package/dist/lib/constants/contextWindows.js +107 -16
- package/dist/lib/constants/enums.d.ts +99 -15
- package/dist/lib/constants/enums.js +152 -22
- package/dist/lib/context/budgetChecker.js +1 -1
- package/dist/lib/context/contextCompactor.js +31 -4
- package/dist/lib/context/emergencyTruncation.d.ts +21 -0
- package/dist/lib/context/emergencyTruncation.js +89 -0
- package/dist/lib/context/errorDetection.d.ts +16 -0
- package/dist/lib/context/errorDetection.js +48 -1
- package/dist/lib/context/errors.d.ts +19 -0
- package/dist/lib/context/errors.js +22 -0
- package/dist/lib/context/stages/slidingWindowTruncator.d.ts +6 -0
- package/dist/lib/context/stages/slidingWindowTruncator.js +159 -24
- package/dist/lib/core/baseProvider.js +306 -200
- package/dist/lib/core/conversationMemoryManager.js +104 -61
- package/dist/lib/core/evaluationProviders.js +16 -33
- package/dist/lib/core/factory.js +237 -164
- package/dist/lib/core/modules/GenerationHandler.js +175 -116
- package/dist/lib/core/modules/MessageBuilder.js +222 -170
- package/dist/lib/core/modules/StreamHandler.d.ts +1 -0
- package/dist/lib/core/modules/StreamHandler.js +95 -27
- package/dist/lib/core/modules/TelemetryHandler.d.ts +10 -1
- package/dist/lib/core/modules/TelemetryHandler.js +25 -7
- package/dist/lib/core/modules/ToolsManager.js +115 -191
- package/dist/lib/core/redisConversationMemoryManager.js +418 -282
- package/dist/lib/factories/providerRegistry.d.ts +5 -0
- package/dist/lib/factories/providerRegistry.js +20 -2
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.js +4 -2
- package/dist/lib/mcp/externalServerManager.js +66 -0
- package/dist/lib/mcp/mcpCircuitBreaker.js +24 -0
- package/dist/lib/mcp/mcpClientFactory.js +16 -0
- package/dist/lib/mcp/toolDiscoveryService.js +32 -6
- package/dist/lib/mcp/toolRegistry.js +193 -123
- package/dist/lib/neurolink.d.ts +6 -0
- package/dist/lib/neurolink.js +1162 -646
- package/dist/lib/providers/amazonBedrock.d.ts +1 -1
- package/dist/lib/providers/amazonBedrock.js +521 -319
- package/dist/lib/providers/anthropic.js +73 -17
- package/dist/lib/providers/anthropicBaseProvider.js +77 -17
- package/dist/lib/providers/googleAiStudio.d.ts +1 -1
- package/dist/lib/providers/googleAiStudio.js +292 -227
- package/dist/lib/providers/googleVertex.d.ts +36 -1
- package/dist/lib/providers/googleVertex.js +553 -260
- package/dist/lib/providers/ollama.js +329 -278
- package/dist/lib/providers/openAI.js +77 -19
- package/dist/lib/providers/sagemaker/parsers.js +3 -3
- package/dist/lib/providers/sagemaker/streaming.js +3 -3
- package/dist/lib/proxy/proxyFetch.js +81 -48
- package/dist/lib/rag/ChunkerFactory.js +1 -1
- package/dist/lib/rag/chunkers/MarkdownChunker.d.ts +22 -0
- package/dist/lib/rag/chunkers/MarkdownChunker.js +213 -9
- package/dist/lib/rag/chunking/markdownChunker.d.ts +16 -0
- package/dist/lib/rag/chunking/markdownChunker.js +174 -2
- package/dist/lib/rag/pipeline/contextAssembly.js +2 -1
- package/dist/lib/rag/ragIntegration.d.ts +18 -1
- package/dist/lib/rag/ragIntegration.js +94 -14
- package/dist/lib/rag/retrieval/vectorQueryTool.js +21 -4
- package/dist/lib/server/abstract/baseServerAdapter.js +4 -1
- package/dist/lib/server/adapters/fastifyAdapter.js +35 -30
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +32 -0
- package/dist/lib/services/server/ai/observability/instrumentation.js +39 -0
- package/dist/lib/telemetry/attributes.d.ts +52 -0
- package/dist/lib/telemetry/attributes.js +61 -0
- package/dist/lib/telemetry/index.d.ts +3 -0
- package/dist/lib/telemetry/index.js +3 -0
- package/dist/lib/telemetry/telemetryService.d.ts +6 -0
- package/dist/lib/telemetry/telemetryService.js +6 -0
- package/dist/lib/telemetry/tracers.d.ts +15 -0
- package/dist/lib/telemetry/tracers.js +17 -0
- package/dist/lib/telemetry/withSpan.d.ts +9 -0
- package/dist/lib/telemetry/withSpan.js +35 -0
- package/dist/lib/types/contextTypes.d.ts +10 -0
- package/dist/lib/types/streamTypes.d.ts +14 -0
- package/dist/lib/utils/conversationMemory.js +121 -82
- package/dist/lib/utils/logger.d.ts +5 -0
- package/dist/lib/utils/logger.js +50 -2
- package/dist/lib/utils/messageBuilder.js +22 -42
- package/dist/lib/utils/modelDetection.js +3 -3
- package/dist/lib/utils/providerRetry.d.ts +41 -0
- package/dist/lib/utils/providerRetry.js +114 -0
- package/dist/lib/utils/retryability.d.ts +14 -0
- package/dist/lib/utils/retryability.js +23 -0
- package/dist/lib/utils/sanitizers/svg.js +4 -5
- package/dist/lib/utils/tokenEstimation.d.ts +11 -1
- package/dist/lib/utils/tokenEstimation.js +19 -4
- package/dist/lib/utils/videoAnalysisProcessor.js +7 -3
- package/dist/mcp/externalServerManager.js +66 -0
- package/dist/mcp/mcpCircuitBreaker.js +24 -0
- package/dist/mcp/mcpClientFactory.js +16 -0
- package/dist/mcp/toolDiscoveryService.js +32 -6
- package/dist/mcp/toolRegistry.js +193 -123
- package/dist/neurolink.d.ts +6 -0
- package/dist/neurolink.js +1162 -646
- package/dist/providers/amazonBedrock.d.ts +1 -1
- package/dist/providers/amazonBedrock.js +521 -319
- package/dist/providers/anthropic.js +73 -17
- package/dist/providers/anthropicBaseProvider.js +77 -17
- package/dist/providers/googleAiStudio.d.ts +1 -1
- package/dist/providers/googleAiStudio.js +292 -227
- package/dist/providers/googleVertex.d.ts +36 -1
- package/dist/providers/googleVertex.js +553 -260
- package/dist/providers/ollama.js +329 -278
- package/dist/providers/openAI.js +77 -19
- package/dist/providers/sagemaker/parsers.js +3 -3
- package/dist/providers/sagemaker/streaming.js +3 -3
- package/dist/proxy/proxyFetch.js +81 -48
- package/dist/rag/ChunkerFactory.js +1 -1
- package/dist/rag/chunkers/MarkdownChunker.d.ts +22 -0
- package/dist/rag/chunkers/MarkdownChunker.js +213 -9
- package/dist/rag/chunking/markdownChunker.d.ts +16 -0
- package/dist/rag/chunking/markdownChunker.js +174 -2
- package/dist/rag/pipeline/contextAssembly.js +2 -1
- package/dist/rag/ragIntegration.d.ts +18 -1
- package/dist/rag/ragIntegration.js +94 -14
- package/dist/rag/retrieval/vectorQueryTool.js +21 -4
- package/dist/server/abstract/baseServerAdapter.js +4 -1
- package/dist/server/adapters/fastifyAdapter.js +35 -30
- package/dist/services/server/ai/observability/instrumentation.d.ts +32 -0
- package/dist/services/server/ai/observability/instrumentation.js +39 -0
- package/dist/telemetry/attributes.d.ts +52 -0
- package/dist/telemetry/attributes.js +60 -0
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.js +3 -0
- package/dist/telemetry/telemetryService.d.ts +6 -0
- package/dist/telemetry/telemetryService.js +6 -0
- package/dist/telemetry/tracers.d.ts +15 -0
- package/dist/telemetry/tracers.js +16 -0
- package/dist/telemetry/withSpan.d.ts +9 -0
- package/dist/telemetry/withSpan.js +34 -0
- package/dist/types/contextTypes.d.ts +10 -0
- package/dist/types/streamTypes.d.ts +14 -0
- package/dist/utils/conversationMemory.js +121 -82
- package/dist/utils/logger.d.ts +5 -0
- package/dist/utils/logger.js +50 -2
- package/dist/utils/messageBuilder.js +22 -42
- package/dist/utils/modelDetection.js +3 -3
- package/dist/utils/providerRetry.d.ts +41 -0
- package/dist/utils/providerRetry.js +113 -0
- package/dist/utils/retryability.d.ts +14 -0
- package/dist/utils/retryability.js +22 -0
- package/dist/utils/sanitizers/svg.js +4 -5
- package/dist/utils/tokenEstimation.d.ts +11 -1
- package/dist/utils/tokenEstimation.js +19 -4
- package/dist/utils/videoAnalysisProcessor.js +7 -3
- package/dist/workflow/config.d.ts +26 -26
- package/package.json +1 -1
|
@@ -12,8 +12,45 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module core/modules/MessageBuilder
|
|
14
14
|
*/
|
|
15
|
+
import { tracers, ATTR, withSpan } from "../../telemetry/index.js";
|
|
15
16
|
import { logger } from "../../utils/logger.js";
|
|
16
17
|
import { buildMessagesArray, buildMultimodalMessagesArray, } from "../../utils/messageBuilder.js";
|
|
18
|
+
/**
|
|
19
|
+
* Compute total content length across all messages for span attributes.
|
|
20
|
+
*/
|
|
21
|
+
function computeTotalContentLength(messages) {
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (const msg of messages) {
|
|
24
|
+
if (typeof msg.content === "string") {
|
|
25
|
+
total += msg.content.length;
|
|
26
|
+
}
|
|
27
|
+
else if (Array.isArray(msg.content)) {
|
|
28
|
+
for (const part of msg.content) {
|
|
29
|
+
if ("text" in part &&
|
|
30
|
+
typeof part.text === "string") {
|
|
31
|
+
total += part.text.length;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return total;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check whether input contains multimodal content (images, files, PDFs, CSVs).
|
|
40
|
+
*/
|
|
41
|
+
function detectMultimodal(opts) {
|
|
42
|
+
const input = opts.input;
|
|
43
|
+
const hasImages = !!input?.images?.length;
|
|
44
|
+
const hasContent = !!input?.content?.length;
|
|
45
|
+
const hasCSVFiles = !!input?.csvFiles?.length;
|
|
46
|
+
const hasPdfFiles = !!input?.pdfFiles?.length;
|
|
47
|
+
const hasFiles = !!input?.files?.length;
|
|
48
|
+
return {
|
|
49
|
+
isMultimodal: hasImages || hasContent || hasCSVFiles || hasPdfFiles || hasFiles,
|
|
50
|
+
hasImages,
|
|
51
|
+
hasFiles: hasCSVFiles || hasPdfFiles || hasFiles,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
17
54
|
/**
|
|
18
55
|
* MessageBuilder class - Handles message construction for AI providers
|
|
19
56
|
*/
|
|
@@ -29,93 +66,101 @@ export class MessageBuilder {
|
|
|
29
66
|
* Detects multimodal input and routes to appropriate message builder
|
|
30
67
|
*/
|
|
31
68
|
async buildMessages(options) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
messages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
74
|
-
logger.debug("No multimodal input detected, using standard message builder");
|
|
75
|
-
}
|
|
76
|
-
messages = await buildMessagesArray(options);
|
|
77
|
-
}
|
|
78
|
-
// Convert messages to Vercel AI SDK format
|
|
79
|
-
// Preserve providerOptions (e.g. Anthropic cache_control) through conversion
|
|
80
|
-
return messages.map((msg) => {
|
|
81
|
-
const providerOptions = msg
|
|
82
|
-
.providerOptions;
|
|
83
|
-
if (typeof msg.content === "string") {
|
|
84
|
-
return {
|
|
85
|
-
role: msg.role,
|
|
86
|
-
content: msg.content,
|
|
87
|
-
...(providerOptions && { providerOptions }),
|
|
69
|
+
return withSpan({
|
|
70
|
+
name: "neurolink.message.build",
|
|
71
|
+
tracer: tracers.sdk,
|
|
72
|
+
attributes: {
|
|
73
|
+
[ATTR.NL_PROVIDER]: this.providerName,
|
|
74
|
+
[ATTR.NL_MODEL]: this.modelName,
|
|
75
|
+
},
|
|
76
|
+
}, async (span) => {
|
|
77
|
+
const { isMultimodal, hasImages, hasFiles } = detectMultimodal(options);
|
|
78
|
+
span.setAttribute(ATTR.MSG_IS_MULTIMODAL, isMultimodal);
|
|
79
|
+
let messages;
|
|
80
|
+
if (isMultimodal) {
|
|
81
|
+
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
82
|
+
logger.debug("Detected multimodal input, using multimodal message builder");
|
|
83
|
+
}
|
|
84
|
+
const input = options.input;
|
|
85
|
+
const multimodalOptions = {
|
|
86
|
+
input: {
|
|
87
|
+
text: options.prompt || options.input?.text || "",
|
|
88
|
+
images: input?.images,
|
|
89
|
+
content: input?.content,
|
|
90
|
+
csvFiles: input?.csvFiles,
|
|
91
|
+
pdfFiles: input?.pdfFiles,
|
|
92
|
+
files: input?.files,
|
|
93
|
+
},
|
|
94
|
+
csvOptions: options.csvOptions,
|
|
95
|
+
provider: options.provider,
|
|
96
|
+
model: options.model,
|
|
97
|
+
temperature: options.temperature,
|
|
98
|
+
maxTokens: options.maxTokens,
|
|
99
|
+
systemPrompt: options.systemPrompt,
|
|
100
|
+
enableAnalytics: options.enableAnalytics,
|
|
101
|
+
enableEvaluation: options.enableEvaluation,
|
|
102
|
+
context: options.context,
|
|
103
|
+
conversationHistory: options.conversationMessages,
|
|
104
|
+
schema: options.schema,
|
|
105
|
+
output: options.output,
|
|
106
|
+
fileRegistry: options.fileRegistry,
|
|
88
107
|
};
|
|
108
|
+
messages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
|
|
89
109
|
}
|
|
90
110
|
else {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.providerOptions;
|
|
96
|
-
if (item.type === "text") {
|
|
97
|
-
return {
|
|
98
|
-
type: "text",
|
|
99
|
-
text: item.text || "",
|
|
100
|
-
...(itemProviderOptions && {
|
|
101
|
-
providerOptions: itemProviderOptions,
|
|
102
|
-
}),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
else if (item.type === "image") {
|
|
106
|
-
return {
|
|
107
|
-
type: "image",
|
|
108
|
-
image: item.image || "",
|
|
109
|
-
...(itemProviderOptions && {
|
|
110
|
-
providerOptions: itemProviderOptions,
|
|
111
|
-
}),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return item;
|
|
115
|
-
}),
|
|
116
|
-
...(providerOptions && { providerOptions }),
|
|
117
|
-
};
|
|
111
|
+
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
112
|
+
logger.debug("No multimodal input detected, using standard message builder");
|
|
113
|
+
}
|
|
114
|
+
messages = await buildMessagesArray(options);
|
|
118
115
|
}
|
|
116
|
+
// Convert messages to Vercel AI SDK format
|
|
117
|
+
// Preserve providerOptions (e.g. Anthropic cache_control) through conversion
|
|
118
|
+
const coreMessages = messages.map((msg) => {
|
|
119
|
+
const providerOptions = msg
|
|
120
|
+
.providerOptions;
|
|
121
|
+
if (typeof msg.content === "string") {
|
|
122
|
+
return {
|
|
123
|
+
role: msg.role,
|
|
124
|
+
content: msg.content,
|
|
125
|
+
...(providerOptions && { providerOptions }),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
return {
|
|
130
|
+
role: msg.role,
|
|
131
|
+
content: msg.content.map((item) => {
|
|
132
|
+
const itemProviderOptions = item
|
|
133
|
+
.providerOptions;
|
|
134
|
+
if (item.type === "text") {
|
|
135
|
+
return {
|
|
136
|
+
type: "text",
|
|
137
|
+
text: item.text || "",
|
|
138
|
+
...(itemProviderOptions && {
|
|
139
|
+
providerOptions: itemProviderOptions,
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
else if (item.type === "image") {
|
|
144
|
+
return {
|
|
145
|
+
type: "image",
|
|
146
|
+
image: item.image || "",
|
|
147
|
+
...(itemProviderOptions && {
|
|
148
|
+
providerOptions: itemProviderOptions,
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return item;
|
|
153
|
+
}),
|
|
154
|
+
...(providerOptions && { providerOptions }),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
span.setAttribute(ATTR.MSG_COUNT, coreMessages.length);
|
|
159
|
+
span.setAttribute(ATTR.MSG_HAS_IMAGES, hasImages);
|
|
160
|
+
span.setAttribute(ATTR.MSG_HAS_FILES, hasFiles);
|
|
161
|
+
span.setAttribute(ATTR.MSG_HAS_SYSTEM_PROMPT, !!options.systemPrompt);
|
|
162
|
+
span.setAttribute(ATTR.MSG_TOTAL_CONTENT_LENGTH, computeTotalContentLength(coreMessages));
|
|
163
|
+
return coreMessages;
|
|
119
164
|
});
|
|
120
165
|
}
|
|
121
166
|
/**
|
|
@@ -127,97 +172,104 @@ export class MessageBuilder {
|
|
|
127
172
|
* @returns Promise resolving to CoreMessage array ready for AI SDK
|
|
128
173
|
*/
|
|
129
174
|
async buildMessagesForStream(options) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
};
|
|
172
|
-
messages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
176
|
-
logger.debug(`${this.providerName}: No multimodal input detected, using standard message builder`);
|
|
177
|
-
}
|
|
178
|
-
messages = await buildMessagesArray(options);
|
|
179
|
-
}
|
|
180
|
-
// Convert messages to Vercel AI SDK format
|
|
181
|
-
// Preserve providerOptions (e.g. Anthropic cache_control) through conversion
|
|
182
|
-
return messages.map((msg) => {
|
|
183
|
-
const providerOptions = msg
|
|
184
|
-
.providerOptions;
|
|
185
|
-
if (typeof msg.content === "string") {
|
|
186
|
-
return {
|
|
187
|
-
role: msg.role,
|
|
188
|
-
content: msg.content,
|
|
189
|
-
...(providerOptions && { providerOptions }),
|
|
175
|
+
return withSpan({
|
|
176
|
+
name: "neurolink.message.build_for_stream",
|
|
177
|
+
tracer: tracers.sdk,
|
|
178
|
+
attributes: {
|
|
179
|
+
[ATTR.NL_PROVIDER]: this.providerName,
|
|
180
|
+
[ATTR.NL_MODEL]: this.modelName,
|
|
181
|
+
},
|
|
182
|
+
}, async (span) => {
|
|
183
|
+
const { isMultimodal, hasImages, hasFiles } = detectMultimodal(options);
|
|
184
|
+
span.setAttribute(ATTR.MSG_IS_MULTIMODAL, isMultimodal);
|
|
185
|
+
let messages;
|
|
186
|
+
if (isMultimodal) {
|
|
187
|
+
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
188
|
+
logger.debug(`${this.providerName}: Detected multimodal input, using multimodal message builder`);
|
|
189
|
+
}
|
|
190
|
+
const input = options.input;
|
|
191
|
+
const multimodalOptions = {
|
|
192
|
+
input: {
|
|
193
|
+
text: options.prompt ||
|
|
194
|
+
options.input?.text ||
|
|
195
|
+
"",
|
|
196
|
+
images: input?.images,
|
|
197
|
+
content: input?.content,
|
|
198
|
+
csvFiles: input?.csvFiles,
|
|
199
|
+
pdfFiles: input?.pdfFiles,
|
|
200
|
+
files: input?.files,
|
|
201
|
+
},
|
|
202
|
+
csvOptions: options.csvOptions,
|
|
203
|
+
provider: options.provider,
|
|
204
|
+
model: options.model,
|
|
205
|
+
temperature: options.temperature,
|
|
206
|
+
maxTokens: options.maxTokens,
|
|
207
|
+
systemPrompt: options.systemPrompt,
|
|
208
|
+
enableAnalytics: options.enableAnalytics,
|
|
209
|
+
enableEvaluation: options.enableEvaluation,
|
|
210
|
+
context: options.context,
|
|
211
|
+
conversationHistory: options
|
|
212
|
+
.conversationMessages,
|
|
213
|
+
schema: options.schema,
|
|
214
|
+
output: options.output,
|
|
215
|
+
fileRegistry: options.fileRegistry,
|
|
190
216
|
};
|
|
217
|
+
messages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
|
|
191
218
|
}
|
|
192
219
|
else {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.providerOptions;
|
|
198
|
-
if (item.type === "text") {
|
|
199
|
-
return {
|
|
200
|
-
type: "text",
|
|
201
|
-
text: item.text || "",
|
|
202
|
-
...(itemProviderOptions && {
|
|
203
|
-
providerOptions: itemProviderOptions,
|
|
204
|
-
}),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
else if (item.type === "image") {
|
|
208
|
-
return {
|
|
209
|
-
type: "image",
|
|
210
|
-
image: item.image || "",
|
|
211
|
-
...(itemProviderOptions && {
|
|
212
|
-
providerOptions: itemProviderOptions,
|
|
213
|
-
}),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return item;
|
|
217
|
-
}),
|
|
218
|
-
...(providerOptions && { providerOptions }),
|
|
219
|
-
};
|
|
220
|
+
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
221
|
+
logger.debug(`${this.providerName}: No multimodal input detected, using standard message builder`);
|
|
222
|
+
}
|
|
223
|
+
messages = await buildMessagesArray(options);
|
|
220
224
|
}
|
|
225
|
+
// Convert messages to Vercel AI SDK format
|
|
226
|
+
// Preserve providerOptions (e.g. Anthropic cache_control) through conversion
|
|
227
|
+
const coreMessages = messages.map((msg) => {
|
|
228
|
+
const providerOptions = msg
|
|
229
|
+
.providerOptions;
|
|
230
|
+
if (typeof msg.content === "string") {
|
|
231
|
+
return {
|
|
232
|
+
role: msg.role,
|
|
233
|
+
content: msg.content,
|
|
234
|
+
...(providerOptions && { providerOptions }),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
return {
|
|
239
|
+
role: msg.role,
|
|
240
|
+
content: msg.content.map((item) => {
|
|
241
|
+
const itemProviderOptions = item
|
|
242
|
+
.providerOptions;
|
|
243
|
+
if (item.type === "text") {
|
|
244
|
+
return {
|
|
245
|
+
type: "text",
|
|
246
|
+
text: item.text || "",
|
|
247
|
+
...(itemProviderOptions && {
|
|
248
|
+
providerOptions: itemProviderOptions,
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
else if (item.type === "image") {
|
|
253
|
+
return {
|
|
254
|
+
type: "image",
|
|
255
|
+
image: item.image || "",
|
|
256
|
+
...(itemProviderOptions && {
|
|
257
|
+
providerOptions: itemProviderOptions,
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return item;
|
|
262
|
+
}),
|
|
263
|
+
...(providerOptions && { providerOptions }),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
span.setAttribute(ATTR.MSG_COUNT, coreMessages.length);
|
|
268
|
+
span.setAttribute(ATTR.MSG_HAS_IMAGES, hasImages);
|
|
269
|
+
span.setAttribute(ATTR.MSG_HAS_FILES, hasFiles);
|
|
270
|
+
span.setAttribute(ATTR.MSG_HAS_SYSTEM_PROMPT, !!options.systemPrompt);
|
|
271
|
+
span.setAttribute(ATTR.MSG_TOTAL_CONTENT_LENGTH, computeTotalContentLength(coreMessages));
|
|
272
|
+
return coreMessages;
|
|
221
273
|
});
|
|
222
274
|
}
|
|
223
275
|
}
|
|
@@ -28,6 +28,7 @@ export declare class StreamHandler {
|
|
|
28
28
|
validateStreamOptions(options: StreamOptions): void;
|
|
29
29
|
/**
|
|
30
30
|
* Create text stream transformation - consolidates identical logic from 7/10 providers
|
|
31
|
+
* Tracks TTFC (Time To First Chunk), chunk count, and total bytes streamed.
|
|
31
32
|
*/
|
|
32
33
|
createTextStream(result: {
|
|
33
34
|
textStream: AsyncIterable<string>;
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module core/modules/StreamHandler
|
|
14
14
|
*/
|
|
15
|
+
import { trace, context as otelContext, SpanStatusCode, } from "@opentelemetry/api";
|
|
16
|
+
import { tracers, ATTR, withSpan } from "../../telemetry/index.js";
|
|
15
17
|
import { logger } from "../../utils/logger.js";
|
|
16
18
|
import { validateStreamOptions as validateStreamOpts, ValidationError, createValidationSummary, } from "../../utils/parameterValidation.js";
|
|
17
19
|
import { STEP_LIMITS } from "../constants.js";
|
|
@@ -31,33 +33,87 @@ export class StreamHandler {
|
|
|
31
33
|
* Validate stream options - consolidates validation from 7/10 providers
|
|
32
34
|
*/
|
|
33
35
|
validateStreamOptions(options) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const span = tracers.stream.startSpan("neurolink.stream.validate", {
|
|
37
|
+
attributes: {
|
|
38
|
+
[ATTR.NL_PROVIDER]: this.providerName,
|
|
39
|
+
[ATTR.NL_MODEL]: this.modelName,
|
|
40
|
+
"stream.has_max_steps": options.maxSteps !== undefined,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const validation = validateStreamOpts(options);
|
|
45
|
+
if (!validation.isValid) {
|
|
46
|
+
const summary = createValidationSummary(validation);
|
|
47
|
+
span.setAttribute("stream.validation_errors", validation.errors?.length ?? 0);
|
|
48
|
+
throw new ValidationError(`Stream options validation failed: ${summary}`, "options", "VALIDATION_FAILED", validation.suggestions);
|
|
49
|
+
}
|
|
50
|
+
// Log warnings if any
|
|
51
|
+
if (validation.warnings.length > 0) {
|
|
52
|
+
logger.warn("Stream options validation warnings:", validation.warnings);
|
|
53
|
+
span.addEvent("stream.validation.warnings", {
|
|
54
|
+
"warning.count": validation.warnings.length,
|
|
55
|
+
warnings: validation.warnings.join("; ").substring(0, 500),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Additional BaseProvider-specific validation
|
|
59
|
+
if (options.maxSteps !== undefined) {
|
|
60
|
+
if (options.maxSteps < STEP_LIMITS.min ||
|
|
61
|
+
options.maxSteps > STEP_LIMITS.max) {
|
|
62
|
+
throw new ValidationError(`maxSteps must be between ${STEP_LIMITS.min} and ${STEP_LIMITS.max}`, "maxSteps", "OUT_OF_RANGE", [
|
|
63
|
+
`Use a value between ${STEP_LIMITS.min} and ${STEP_LIMITS.max} for optimal performance`,
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
38
67
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
catch (error) {
|
|
69
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
70
|
+
// NLK-GAP-006 fix: set error status alongside recordException
|
|
71
|
+
span.setStatus({
|
|
72
|
+
code: SpanStatusCode.ERROR,
|
|
73
|
+
message: error instanceof Error ? error.message : String(error),
|
|
74
|
+
});
|
|
75
|
+
throw error;
|
|
42
76
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (options.maxSteps < STEP_LIMITS.min ||
|
|
46
|
-
options.maxSteps > STEP_LIMITS.max) {
|
|
47
|
-
throw new ValidationError(`maxSteps must be between ${STEP_LIMITS.min} and ${STEP_LIMITS.max}`, "maxSteps", "OUT_OF_RANGE", [
|
|
48
|
-
`Use a value between ${STEP_LIMITS.min} and ${STEP_LIMITS.max} for optimal performance`,
|
|
49
|
-
]);
|
|
50
|
-
}
|
|
77
|
+
finally {
|
|
78
|
+
span.end();
|
|
51
79
|
}
|
|
52
80
|
}
|
|
53
81
|
/**
|
|
54
82
|
* Create text stream transformation - consolidates identical logic from 7/10 providers
|
|
83
|
+
* Tracks TTFC (Time To First Chunk), chunk count, and total bytes streamed.
|
|
55
84
|
*/
|
|
56
85
|
createTextStream(result) {
|
|
86
|
+
const providerName = this.providerName;
|
|
57
87
|
return (async function* () {
|
|
88
|
+
let chunkCount = 0;
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
const streamStart = Date.now();
|
|
91
|
+
let firstChunkTime;
|
|
58
92
|
for await (const chunk of result.textStream) {
|
|
93
|
+
chunkCount++;
|
|
94
|
+
totalBytes += chunk.length;
|
|
95
|
+
if (!firstChunkTime) {
|
|
96
|
+
firstChunkTime = Date.now();
|
|
97
|
+
const activeSpan = trace.getSpan(otelContext.active());
|
|
98
|
+
if (activeSpan) {
|
|
99
|
+
activeSpan.addEvent("stream.first_chunk", {
|
|
100
|
+
"stream.ttfc_ms": firstChunkTime - streamStart,
|
|
101
|
+
"stream.provider": providerName,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
59
105
|
yield { content: chunk };
|
|
60
106
|
}
|
|
107
|
+
// Record completion metrics on the active span
|
|
108
|
+
const activeSpan = trace.getSpan(otelContext.active());
|
|
109
|
+
if (activeSpan) {
|
|
110
|
+
activeSpan.addEvent("stream.complete", {
|
|
111
|
+
"stream.chunk_count": chunkCount,
|
|
112
|
+
"stream.total_bytes": totalBytes,
|
|
113
|
+
"stream.duration_ms": Date.now() - streamStart,
|
|
114
|
+
"stream.ttfc_ms": firstChunkTime ? firstChunkTime - streamStart : -1,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
61
117
|
})();
|
|
62
118
|
}
|
|
63
119
|
/**
|
|
@@ -75,18 +131,30 @@ export class StreamHandler {
|
|
|
75
131
|
* Create stream analytics - consolidates analytics from 4/10 providers
|
|
76
132
|
*/
|
|
77
133
|
async createStreamAnalytics(result, startTime, options) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
134
|
+
return withSpan({
|
|
135
|
+
name: "neurolink.stream.analytics",
|
|
136
|
+
tracer: tracers.stream,
|
|
137
|
+
attributes: {
|
|
138
|
+
[ATTR.NL_PROVIDER]: this.providerName,
|
|
139
|
+
[ATTR.NL_MODEL]: this.modelName,
|
|
140
|
+
[ATTR.NL_STREAM_MODE]: true,
|
|
141
|
+
},
|
|
142
|
+
}, async (span) => {
|
|
143
|
+
try {
|
|
144
|
+
const durationMs = Date.now() - startTime;
|
|
145
|
+
span.setAttribute("stream.duration_ms", durationMs);
|
|
146
|
+
const analytics = createAnalytics(this.providerName, this.modelName, result, durationMs, {
|
|
147
|
+
requestId: `${this.providerName}-stream-${nanoid()}`,
|
|
148
|
+
streamingMode: true,
|
|
149
|
+
...options.context,
|
|
150
|
+
});
|
|
151
|
+
return analytics;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
logger.warn(`Analytics creation failed for ${this.providerName}:`, error);
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
90
158
|
}
|
|
91
159
|
/**
|
|
92
160
|
* Validate streaming-only options (called before executeStream)
|
|
@@ -41,7 +41,16 @@ export declare class TelemetryHandler {
|
|
|
41
41
|
totalTokens: number;
|
|
42
42
|
} | undefined, responseTime: number): Promise<void>;
|
|
43
43
|
/**
|
|
44
|
-
* Calculate actual cost based on token usage and provider configuration
|
|
44
|
+
* Calculate actual cost based on token usage and provider configuration.
|
|
45
|
+
*
|
|
46
|
+
* Uses the per-model pricing table first (which has accurate rates for
|
|
47
|
+
* specific models like Claude on Vertex AI), then falls back to the
|
|
48
|
+
* provider-level default cost from modelConfiguration.
|
|
49
|
+
*
|
|
50
|
+
* Previously this only used modelConfig.getCostInfo() which returns
|
|
51
|
+
* provider-level defaults (e.g. Gemini rates for the "vertex" provider),
|
|
52
|
+
* causing a ~1,780x under-estimate when the actual model was Claude Sonnet
|
|
53
|
+
* on Vertex AI ($0.000060 vs $0.106895 for the same request).
|
|
45
54
|
*/
|
|
46
55
|
calculateActualCost(usage: {
|
|
47
56
|
promptTokens?: number;
|