@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.
Files changed (193) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
  3. package/dist/adapters/video/videoAnalyzer.js +10 -8
  4. package/dist/cli/commands/setup-anthropic.js +1 -14
  5. package/dist/cli/commands/setup-azure.js +1 -12
  6. package/dist/cli/commands/setup-bedrock.js +1 -9
  7. package/dist/cli/commands/setup-google-ai.js +1 -12
  8. package/dist/cli/commands/setup-openai.js +1 -14
  9. package/dist/cli/commands/workflow.d.ts +27 -0
  10. package/dist/cli/commands/workflow.js +216 -0
  11. package/dist/cli/factories/commandFactory.js +79 -20
  12. package/dist/cli/index.js +0 -1
  13. package/dist/cli/parser.js +4 -1
  14. package/dist/cli/utils/maskCredential.d.ts +11 -0
  15. package/dist/cli/utils/maskCredential.js +23 -0
  16. package/dist/constants/contextWindows.js +107 -16
  17. package/dist/constants/enums.d.ts +99 -15
  18. package/dist/constants/enums.js +152 -22
  19. package/dist/context/budgetChecker.js +1 -1
  20. package/dist/context/contextCompactor.js +31 -4
  21. package/dist/context/emergencyTruncation.d.ts +21 -0
  22. package/dist/context/emergencyTruncation.js +88 -0
  23. package/dist/context/errorDetection.d.ts +16 -0
  24. package/dist/context/errorDetection.js +48 -1
  25. package/dist/context/errors.d.ts +19 -0
  26. package/dist/context/errors.js +21 -0
  27. package/dist/context/stages/slidingWindowTruncator.d.ts +6 -0
  28. package/dist/context/stages/slidingWindowTruncator.js +159 -24
  29. package/dist/core/baseProvider.js +306 -200
  30. package/dist/core/conversationMemoryManager.js +104 -61
  31. package/dist/core/evaluationProviders.js +16 -33
  32. package/dist/core/factory.js +237 -164
  33. package/dist/core/modules/GenerationHandler.js +175 -116
  34. package/dist/core/modules/MessageBuilder.js +222 -170
  35. package/dist/core/modules/StreamHandler.d.ts +1 -0
  36. package/dist/core/modules/StreamHandler.js +95 -27
  37. package/dist/core/modules/TelemetryHandler.d.ts +10 -1
  38. package/dist/core/modules/TelemetryHandler.js +25 -7
  39. package/dist/core/modules/ToolsManager.js +115 -191
  40. package/dist/core/redisConversationMemoryManager.js +418 -282
  41. package/dist/factories/providerRegistry.d.ts +5 -0
  42. package/dist/factories/providerRegistry.js +20 -2
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +4 -2
  45. package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
  46. package/dist/lib/adapters/video/videoAnalyzer.js +10 -8
  47. package/dist/lib/constants/contextWindows.js +107 -16
  48. package/dist/lib/constants/enums.d.ts +99 -15
  49. package/dist/lib/constants/enums.js +152 -22
  50. package/dist/lib/context/budgetChecker.js +1 -1
  51. package/dist/lib/context/contextCompactor.js +31 -4
  52. package/dist/lib/context/emergencyTruncation.d.ts +21 -0
  53. package/dist/lib/context/emergencyTruncation.js +89 -0
  54. package/dist/lib/context/errorDetection.d.ts +16 -0
  55. package/dist/lib/context/errorDetection.js +48 -1
  56. package/dist/lib/context/errors.d.ts +19 -0
  57. package/dist/lib/context/errors.js +22 -0
  58. package/dist/lib/context/stages/slidingWindowTruncator.d.ts +6 -0
  59. package/dist/lib/context/stages/slidingWindowTruncator.js +159 -24
  60. package/dist/lib/core/baseProvider.js +306 -200
  61. package/dist/lib/core/conversationMemoryManager.js +104 -61
  62. package/dist/lib/core/evaluationProviders.js +16 -33
  63. package/dist/lib/core/factory.js +237 -164
  64. package/dist/lib/core/modules/GenerationHandler.js +175 -116
  65. package/dist/lib/core/modules/MessageBuilder.js +222 -170
  66. package/dist/lib/core/modules/StreamHandler.d.ts +1 -0
  67. package/dist/lib/core/modules/StreamHandler.js +95 -27
  68. package/dist/lib/core/modules/TelemetryHandler.d.ts +10 -1
  69. package/dist/lib/core/modules/TelemetryHandler.js +25 -7
  70. package/dist/lib/core/modules/ToolsManager.js +115 -191
  71. package/dist/lib/core/redisConversationMemoryManager.js +418 -282
  72. package/dist/lib/factories/providerRegistry.d.ts +5 -0
  73. package/dist/lib/factories/providerRegistry.js +20 -2
  74. package/dist/lib/index.d.ts +2 -2
  75. package/dist/lib/index.js +4 -2
  76. package/dist/lib/mcp/externalServerManager.js +66 -0
  77. package/dist/lib/mcp/mcpCircuitBreaker.js +24 -0
  78. package/dist/lib/mcp/mcpClientFactory.js +16 -0
  79. package/dist/lib/mcp/toolDiscoveryService.js +32 -6
  80. package/dist/lib/mcp/toolRegistry.js +193 -123
  81. package/dist/lib/neurolink.d.ts +6 -0
  82. package/dist/lib/neurolink.js +1162 -646
  83. package/dist/lib/providers/amazonBedrock.d.ts +1 -1
  84. package/dist/lib/providers/amazonBedrock.js +521 -319
  85. package/dist/lib/providers/anthropic.js +73 -17
  86. package/dist/lib/providers/anthropicBaseProvider.js +77 -17
  87. package/dist/lib/providers/googleAiStudio.d.ts +1 -1
  88. package/dist/lib/providers/googleAiStudio.js +292 -227
  89. package/dist/lib/providers/googleVertex.d.ts +36 -1
  90. package/dist/lib/providers/googleVertex.js +553 -260
  91. package/dist/lib/providers/ollama.js +329 -278
  92. package/dist/lib/providers/openAI.js +77 -19
  93. package/dist/lib/providers/sagemaker/parsers.js +3 -3
  94. package/dist/lib/providers/sagemaker/streaming.js +3 -3
  95. package/dist/lib/proxy/proxyFetch.js +81 -48
  96. package/dist/lib/rag/ChunkerFactory.js +1 -1
  97. package/dist/lib/rag/chunkers/MarkdownChunker.d.ts +22 -0
  98. package/dist/lib/rag/chunkers/MarkdownChunker.js +213 -9
  99. package/dist/lib/rag/chunking/markdownChunker.d.ts +16 -0
  100. package/dist/lib/rag/chunking/markdownChunker.js +174 -2
  101. package/dist/lib/rag/pipeline/contextAssembly.js +2 -1
  102. package/dist/lib/rag/ragIntegration.d.ts +18 -1
  103. package/dist/lib/rag/ragIntegration.js +94 -14
  104. package/dist/lib/rag/retrieval/vectorQueryTool.js +21 -4
  105. package/dist/lib/server/abstract/baseServerAdapter.js +4 -1
  106. package/dist/lib/server/adapters/fastifyAdapter.js +35 -30
  107. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +32 -0
  108. package/dist/lib/services/server/ai/observability/instrumentation.js +39 -0
  109. package/dist/lib/telemetry/attributes.d.ts +52 -0
  110. package/dist/lib/telemetry/attributes.js +61 -0
  111. package/dist/lib/telemetry/index.d.ts +3 -0
  112. package/dist/lib/telemetry/index.js +3 -0
  113. package/dist/lib/telemetry/telemetryService.d.ts +6 -0
  114. package/dist/lib/telemetry/telemetryService.js +6 -0
  115. package/dist/lib/telemetry/tracers.d.ts +15 -0
  116. package/dist/lib/telemetry/tracers.js +17 -0
  117. package/dist/lib/telemetry/withSpan.d.ts +9 -0
  118. package/dist/lib/telemetry/withSpan.js +35 -0
  119. package/dist/lib/types/contextTypes.d.ts +10 -0
  120. package/dist/lib/types/streamTypes.d.ts +14 -0
  121. package/dist/lib/utils/conversationMemory.js +121 -82
  122. package/dist/lib/utils/logger.d.ts +5 -0
  123. package/dist/lib/utils/logger.js +50 -2
  124. package/dist/lib/utils/messageBuilder.js +22 -42
  125. package/dist/lib/utils/modelDetection.js +3 -3
  126. package/dist/lib/utils/providerRetry.d.ts +41 -0
  127. package/dist/lib/utils/providerRetry.js +114 -0
  128. package/dist/lib/utils/retryability.d.ts +14 -0
  129. package/dist/lib/utils/retryability.js +23 -0
  130. package/dist/lib/utils/sanitizers/svg.js +4 -5
  131. package/dist/lib/utils/tokenEstimation.d.ts +11 -1
  132. package/dist/lib/utils/tokenEstimation.js +19 -4
  133. package/dist/lib/utils/videoAnalysisProcessor.js +7 -3
  134. package/dist/mcp/externalServerManager.js +66 -0
  135. package/dist/mcp/mcpCircuitBreaker.js +24 -0
  136. package/dist/mcp/mcpClientFactory.js +16 -0
  137. package/dist/mcp/toolDiscoveryService.js +32 -6
  138. package/dist/mcp/toolRegistry.js +193 -123
  139. package/dist/neurolink.d.ts +6 -0
  140. package/dist/neurolink.js +1162 -646
  141. package/dist/providers/amazonBedrock.d.ts +1 -1
  142. package/dist/providers/amazonBedrock.js +521 -319
  143. package/dist/providers/anthropic.js +73 -17
  144. package/dist/providers/anthropicBaseProvider.js +77 -17
  145. package/dist/providers/googleAiStudio.d.ts +1 -1
  146. package/dist/providers/googleAiStudio.js +292 -227
  147. package/dist/providers/googleVertex.d.ts +36 -1
  148. package/dist/providers/googleVertex.js +553 -260
  149. package/dist/providers/ollama.js +329 -278
  150. package/dist/providers/openAI.js +77 -19
  151. package/dist/providers/sagemaker/parsers.js +3 -3
  152. package/dist/providers/sagemaker/streaming.js +3 -3
  153. package/dist/proxy/proxyFetch.js +81 -48
  154. package/dist/rag/ChunkerFactory.js +1 -1
  155. package/dist/rag/chunkers/MarkdownChunker.d.ts +22 -0
  156. package/dist/rag/chunkers/MarkdownChunker.js +213 -9
  157. package/dist/rag/chunking/markdownChunker.d.ts +16 -0
  158. package/dist/rag/chunking/markdownChunker.js +174 -2
  159. package/dist/rag/pipeline/contextAssembly.js +2 -1
  160. package/dist/rag/ragIntegration.d.ts +18 -1
  161. package/dist/rag/ragIntegration.js +94 -14
  162. package/dist/rag/retrieval/vectorQueryTool.js +21 -4
  163. package/dist/server/abstract/baseServerAdapter.js +4 -1
  164. package/dist/server/adapters/fastifyAdapter.js +35 -30
  165. package/dist/services/server/ai/observability/instrumentation.d.ts +32 -0
  166. package/dist/services/server/ai/observability/instrumentation.js +39 -0
  167. package/dist/telemetry/attributes.d.ts +52 -0
  168. package/dist/telemetry/attributes.js +60 -0
  169. package/dist/telemetry/index.d.ts +3 -0
  170. package/dist/telemetry/index.js +3 -0
  171. package/dist/telemetry/telemetryService.d.ts +6 -0
  172. package/dist/telemetry/telemetryService.js +6 -0
  173. package/dist/telemetry/tracers.d.ts +15 -0
  174. package/dist/telemetry/tracers.js +16 -0
  175. package/dist/telemetry/withSpan.d.ts +9 -0
  176. package/dist/telemetry/withSpan.js +34 -0
  177. package/dist/types/contextTypes.d.ts +10 -0
  178. package/dist/types/streamTypes.d.ts +14 -0
  179. package/dist/utils/conversationMemory.js +121 -82
  180. package/dist/utils/logger.d.ts +5 -0
  181. package/dist/utils/logger.js +50 -2
  182. package/dist/utils/messageBuilder.js +22 -42
  183. package/dist/utils/modelDetection.js +3 -3
  184. package/dist/utils/providerRetry.d.ts +41 -0
  185. package/dist/utils/providerRetry.js +113 -0
  186. package/dist/utils/retryability.d.ts +14 -0
  187. package/dist/utils/retryability.js +22 -0
  188. package/dist/utils/sanitizers/svg.js +4 -5
  189. package/dist/utils/tokenEstimation.d.ts +11 -1
  190. package/dist/utils/tokenEstimation.js +19 -4
  191. package/dist/utils/videoAnalysisProcessor.js +7 -3
  192. package/dist/workflow/config.d.ts +26 -26
  193. 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
- const hasMultimodalInput = (opts) => {
33
- const input = opts.input;
34
- const hasImages = !!input?.images?.length;
35
- const hasContent = !!input?.content?.length;
36
- const hasCSVFiles = !!input?.csvFiles?.length;
37
- const hasPdfFiles = !!input?.pdfFiles?.length;
38
- const hasFiles = !!input?.files?.length;
39
- return hasImages || hasContent || hasCSVFiles || hasPdfFiles || hasFiles;
40
- };
41
- let messages;
42
- if (hasMultimodalInput(options)) {
43
- if (process.env.NEUROLINK_DEBUG === "true") {
44
- logger.debug("Detected multimodal input, using multimodal message builder");
45
- }
46
- const input = options.input;
47
- const multimodalOptions = {
48
- input: {
49
- text: options.prompt || options.input?.text || "",
50
- images: input?.images,
51
- content: input?.content,
52
- csvFiles: input?.csvFiles,
53
- pdfFiles: input?.pdfFiles,
54
- files: input?.files,
55
- },
56
- csvOptions: options.csvOptions,
57
- provider: options.provider,
58
- model: options.model,
59
- temperature: options.temperature,
60
- maxTokens: options.maxTokens,
61
- systemPrompt: options.systemPrompt,
62
- enableAnalytics: options.enableAnalytics,
63
- enableEvaluation: options.enableEvaluation,
64
- context: options.context,
65
- conversationHistory: options.conversationMessages,
66
- schema: options.schema,
67
- output: options.output,
68
- fileRegistry: options.fileRegistry,
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
- return {
92
- role: msg.role,
93
- content: msg.content.map((item) => {
94
- const itemProviderOptions = item
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
- // Detect multimodal input
131
- const hasMultimodalInput = (opts) => {
132
- const input = opts.input;
133
- const hasImages = !!input?.images?.length;
134
- const hasContent = !!input?.content?.length;
135
- const hasCSVFiles = !!input?.csvFiles?.length;
136
- const hasPdfFiles = !!input?.pdfFiles?.length;
137
- const hasFiles = !!input?.files?.length;
138
- return hasImages || hasContent || hasCSVFiles || hasPdfFiles || hasFiles;
139
- };
140
- let messages;
141
- if (hasMultimodalInput(options)) {
142
- if (process.env.NEUROLINK_DEBUG === "true") {
143
- logger.debug(`${this.providerName}: Detected multimodal input, using multimodal message builder`);
144
- }
145
- const input = options.input;
146
- const multimodalOptions = {
147
- input: {
148
- text: options.prompt ||
149
- options.input?.text ||
150
- "",
151
- images: input?.images,
152
- content: input?.content,
153
- csvFiles: input?.csvFiles,
154
- pdfFiles: input?.pdfFiles,
155
- files: input?.files,
156
- },
157
- csvOptions: options.csvOptions,
158
- provider: options.provider,
159
- model: options.model,
160
- temperature: options.temperature,
161
- maxTokens: options.maxTokens,
162
- systemPrompt: options.systemPrompt,
163
- enableAnalytics: options.enableAnalytics,
164
- enableEvaluation: options.enableEvaluation,
165
- context: options.context,
166
- conversationHistory: options
167
- .conversationMessages,
168
- schema: options.schema,
169
- output: options.output,
170
- fileRegistry: options.fileRegistry,
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
- return {
194
- role: msg.role,
195
- content: msg.content.map((item) => {
196
- const itemProviderOptions = item
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 validation = validateStreamOpts(options);
35
- if (!validation.isValid) {
36
- const summary = createValidationSummary(validation);
37
- throw new ValidationError(`Stream options validation failed: ${summary}`, "options", "VALIDATION_FAILED", validation.suggestions);
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
- // Log warnings if any
40
- if (validation.warnings.length > 0) {
41
- logger.warn("Stream options validation warnings:", validation.warnings);
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
- // Additional BaseProvider-specific validation
44
- if (options.maxSteps !== undefined) {
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
- try {
79
- const analytics = createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
80
- requestId: `${this.providerName}-stream-${nanoid()}`,
81
- streamingMode: true,
82
- ...options.context,
83
- });
84
- return analytics;
85
- }
86
- catch (error) {
87
- logger.warn(`Analytics creation failed for ${this.providerName}:`, error);
88
- return undefined;
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;