@juspay/neurolink 9.14.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 (241) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +15 -15
  3. package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
  4. package/dist/adapters/video/videoAnalyzer.js +10 -8
  5. package/dist/auth/anthropicOAuth.d.ts +377 -0
  6. package/dist/auth/anthropicOAuth.js +914 -0
  7. package/dist/auth/index.d.ts +20 -0
  8. package/dist/auth/index.js +29 -0
  9. package/dist/auth/tokenStore.d.ts +225 -0
  10. package/dist/auth/tokenStore.js +521 -0
  11. package/dist/cli/commands/auth.d.ts +50 -0
  12. package/dist/cli/commands/auth.js +1115 -0
  13. package/dist/cli/commands/setup-anthropic.js +1 -14
  14. package/dist/cli/commands/setup-azure.js +1 -12
  15. package/dist/cli/commands/setup-bedrock.js +1 -9
  16. package/dist/cli/commands/setup-google-ai.js +1 -12
  17. package/dist/cli/commands/setup-openai.js +1 -14
  18. package/dist/cli/commands/workflow.d.ts +27 -0
  19. package/dist/cli/commands/workflow.js +216 -0
  20. package/dist/cli/factories/authCommandFactory.d.ts +52 -0
  21. package/dist/cli/factories/authCommandFactory.js +146 -0
  22. package/dist/cli/factories/commandFactory.d.ts +6 -0
  23. package/dist/cli/factories/commandFactory.js +171 -22
  24. package/dist/cli/index.js +0 -1
  25. package/dist/cli/parser.js +14 -2
  26. package/dist/cli/utils/maskCredential.d.ts +11 -0
  27. package/dist/cli/utils/maskCredential.js +23 -0
  28. package/dist/constants/contextWindows.js +107 -16
  29. package/dist/constants/enums.d.ts +119 -15
  30. package/dist/constants/enums.js +182 -22
  31. package/dist/constants/index.d.ts +3 -1
  32. package/dist/constants/index.js +11 -1
  33. package/dist/context/budgetChecker.js +1 -1
  34. package/dist/context/contextCompactor.js +31 -4
  35. package/dist/context/emergencyTruncation.d.ts +21 -0
  36. package/dist/context/emergencyTruncation.js +88 -0
  37. package/dist/context/errorDetection.d.ts +16 -0
  38. package/dist/context/errorDetection.js +48 -1
  39. package/dist/context/errors.d.ts +19 -0
  40. package/dist/context/errors.js +21 -0
  41. package/dist/context/stages/slidingWindowTruncator.d.ts +6 -0
  42. package/dist/context/stages/slidingWindowTruncator.js +159 -24
  43. package/dist/core/baseProvider.js +306 -200
  44. package/dist/core/conversationMemoryManager.js +104 -61
  45. package/dist/core/evaluationProviders.js +16 -33
  46. package/dist/core/factory.js +237 -164
  47. package/dist/core/modules/GenerationHandler.js +175 -116
  48. package/dist/core/modules/MessageBuilder.js +222 -170
  49. package/dist/core/modules/StreamHandler.d.ts +1 -0
  50. package/dist/core/modules/StreamHandler.js +95 -27
  51. package/dist/core/modules/TelemetryHandler.d.ts +10 -1
  52. package/dist/core/modules/TelemetryHandler.js +25 -7
  53. package/dist/core/modules/ToolsManager.js +115 -191
  54. package/dist/core/redisConversationMemoryManager.js +418 -282
  55. package/dist/factories/providerRegistry.d.ts +5 -0
  56. package/dist/factories/providerRegistry.js +20 -2
  57. package/dist/index.d.ts +3 -3
  58. package/dist/index.js +4 -2
  59. package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
  60. package/dist/lib/adapters/video/videoAnalyzer.js +10 -8
  61. package/dist/lib/auth/anthropicOAuth.d.ts +377 -0
  62. package/dist/lib/auth/anthropicOAuth.js +915 -0
  63. package/dist/lib/auth/index.d.ts +20 -0
  64. package/dist/lib/auth/index.js +30 -0
  65. package/dist/lib/auth/tokenStore.d.ts +225 -0
  66. package/dist/lib/auth/tokenStore.js +522 -0
  67. package/dist/lib/constants/contextWindows.js +107 -16
  68. package/dist/lib/constants/enums.d.ts +119 -15
  69. package/dist/lib/constants/enums.js +182 -22
  70. package/dist/lib/constants/index.d.ts +3 -1
  71. package/dist/lib/constants/index.js +11 -1
  72. package/dist/lib/context/budgetChecker.js +1 -1
  73. package/dist/lib/context/contextCompactor.js +31 -4
  74. package/dist/lib/context/emergencyTruncation.d.ts +21 -0
  75. package/dist/lib/context/emergencyTruncation.js +89 -0
  76. package/dist/lib/context/errorDetection.d.ts +16 -0
  77. package/dist/lib/context/errorDetection.js +48 -1
  78. package/dist/lib/context/errors.d.ts +19 -0
  79. package/dist/lib/context/errors.js +22 -0
  80. package/dist/lib/context/stages/slidingWindowTruncator.d.ts +6 -0
  81. package/dist/lib/context/stages/slidingWindowTruncator.js +159 -24
  82. package/dist/lib/core/baseProvider.js +306 -200
  83. package/dist/lib/core/conversationMemoryManager.js +104 -61
  84. package/dist/lib/core/evaluationProviders.js +16 -33
  85. package/dist/lib/core/factory.js +237 -164
  86. package/dist/lib/core/modules/GenerationHandler.js +175 -116
  87. package/dist/lib/core/modules/MessageBuilder.js +222 -170
  88. package/dist/lib/core/modules/StreamHandler.d.ts +1 -0
  89. package/dist/lib/core/modules/StreamHandler.js +95 -27
  90. package/dist/lib/core/modules/TelemetryHandler.d.ts +10 -1
  91. package/dist/lib/core/modules/TelemetryHandler.js +25 -7
  92. package/dist/lib/core/modules/ToolsManager.js +115 -191
  93. package/dist/lib/core/redisConversationMemoryManager.js +418 -282
  94. package/dist/lib/factories/providerRegistry.d.ts +5 -0
  95. package/dist/lib/factories/providerRegistry.js +20 -2
  96. package/dist/lib/index.d.ts +3 -3
  97. package/dist/lib/index.js +4 -2
  98. package/dist/lib/mcp/externalServerManager.js +66 -0
  99. package/dist/lib/mcp/mcpCircuitBreaker.js +24 -0
  100. package/dist/lib/mcp/mcpClientFactory.js +16 -0
  101. package/dist/lib/mcp/toolDiscoveryService.js +32 -6
  102. package/dist/lib/mcp/toolRegistry.js +193 -123
  103. package/dist/lib/models/anthropicModels.d.ts +267 -0
  104. package/dist/lib/models/anthropicModels.js +528 -0
  105. package/dist/lib/neurolink.d.ts +6 -0
  106. package/dist/lib/neurolink.js +1162 -646
  107. package/dist/lib/providers/amazonBedrock.d.ts +1 -1
  108. package/dist/lib/providers/amazonBedrock.js +521 -319
  109. package/dist/lib/providers/anthropic.d.ts +123 -2
  110. package/dist/lib/providers/anthropic.js +873 -27
  111. package/dist/lib/providers/anthropicBaseProvider.js +77 -17
  112. package/dist/lib/providers/googleAiStudio.d.ts +1 -1
  113. package/dist/lib/providers/googleAiStudio.js +292 -227
  114. package/dist/lib/providers/googleVertex.d.ts +36 -1
  115. package/dist/lib/providers/googleVertex.js +553 -260
  116. package/dist/lib/providers/ollama.js +329 -278
  117. package/dist/lib/providers/openAI.js +77 -19
  118. package/dist/lib/providers/sagemaker/parsers.js +3 -3
  119. package/dist/lib/providers/sagemaker/streaming.js +3 -3
  120. package/dist/lib/proxy/proxyFetch.js +81 -48
  121. package/dist/lib/rag/ChunkerFactory.js +1 -1
  122. package/dist/lib/rag/chunkers/MarkdownChunker.d.ts +22 -0
  123. package/dist/lib/rag/chunkers/MarkdownChunker.js +213 -9
  124. package/dist/lib/rag/chunking/markdownChunker.d.ts +16 -0
  125. package/dist/lib/rag/chunking/markdownChunker.js +174 -2
  126. package/dist/lib/rag/pipeline/contextAssembly.js +2 -1
  127. package/dist/lib/rag/ragIntegration.d.ts +18 -1
  128. package/dist/lib/rag/ragIntegration.js +94 -14
  129. package/dist/lib/rag/retrieval/vectorQueryTool.js +21 -4
  130. package/dist/lib/server/abstract/baseServerAdapter.js +4 -1
  131. package/dist/lib/server/adapters/fastifyAdapter.js +35 -30
  132. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +32 -0
  133. package/dist/lib/services/server/ai/observability/instrumentation.js +39 -0
  134. package/dist/lib/telemetry/attributes.d.ts +52 -0
  135. package/dist/lib/telemetry/attributes.js +61 -0
  136. package/dist/lib/telemetry/index.d.ts +3 -0
  137. package/dist/lib/telemetry/index.js +3 -0
  138. package/dist/lib/telemetry/telemetryService.d.ts +6 -0
  139. package/dist/lib/telemetry/telemetryService.js +6 -0
  140. package/dist/lib/telemetry/tracers.d.ts +15 -0
  141. package/dist/lib/telemetry/tracers.js +17 -0
  142. package/dist/lib/telemetry/withSpan.d.ts +9 -0
  143. package/dist/lib/telemetry/withSpan.js +35 -0
  144. package/dist/lib/types/contextTypes.d.ts +10 -0
  145. package/dist/lib/types/errors.d.ts +62 -0
  146. package/dist/lib/types/errors.js +107 -0
  147. package/dist/lib/types/index.d.ts +2 -1
  148. package/dist/lib/types/index.js +2 -0
  149. package/dist/lib/types/providers.d.ts +107 -0
  150. package/dist/lib/types/providers.js +69 -0
  151. package/dist/lib/types/streamTypes.d.ts +14 -0
  152. package/dist/lib/types/subscriptionTypes.d.ts +893 -0
  153. package/dist/lib/types/subscriptionTypes.js +8 -0
  154. package/dist/lib/utils/conversationMemory.js +121 -82
  155. package/dist/lib/utils/logger.d.ts +5 -0
  156. package/dist/lib/utils/logger.js +50 -2
  157. package/dist/lib/utils/messageBuilder.js +22 -42
  158. package/dist/lib/utils/modelDetection.js +3 -3
  159. package/dist/lib/utils/providerConfig.d.ts +167 -0
  160. package/dist/lib/utils/providerConfig.js +619 -9
  161. package/dist/lib/utils/providerRetry.d.ts +41 -0
  162. package/dist/lib/utils/providerRetry.js +114 -0
  163. package/dist/lib/utils/retryability.d.ts +14 -0
  164. package/dist/lib/utils/retryability.js +23 -0
  165. package/dist/lib/utils/sanitizers/svg.js +4 -5
  166. package/dist/lib/utils/tokenEstimation.d.ts +11 -1
  167. package/dist/lib/utils/tokenEstimation.js +19 -4
  168. package/dist/lib/utils/videoAnalysisProcessor.js +7 -3
  169. package/dist/mcp/externalServerManager.js +66 -0
  170. package/dist/mcp/mcpCircuitBreaker.js +24 -0
  171. package/dist/mcp/mcpClientFactory.js +16 -0
  172. package/dist/mcp/toolDiscoveryService.js +32 -6
  173. package/dist/mcp/toolRegistry.js +193 -123
  174. package/dist/models/anthropicModels.d.ts +267 -0
  175. package/dist/models/anthropicModels.js +527 -0
  176. package/dist/neurolink.d.ts +6 -0
  177. package/dist/neurolink.js +1162 -646
  178. package/dist/providers/amazonBedrock.d.ts +1 -1
  179. package/dist/providers/amazonBedrock.js +521 -319
  180. package/dist/providers/anthropic.d.ts +123 -2
  181. package/dist/providers/anthropic.js +873 -27
  182. package/dist/providers/anthropicBaseProvider.js +77 -17
  183. package/dist/providers/googleAiStudio.d.ts +1 -1
  184. package/dist/providers/googleAiStudio.js +292 -227
  185. package/dist/providers/googleVertex.d.ts +36 -1
  186. package/dist/providers/googleVertex.js +553 -260
  187. package/dist/providers/ollama.js +329 -278
  188. package/dist/providers/openAI.js +77 -19
  189. package/dist/providers/sagemaker/parsers.js +3 -3
  190. package/dist/providers/sagemaker/streaming.js +3 -3
  191. package/dist/proxy/proxyFetch.js +81 -48
  192. package/dist/rag/ChunkerFactory.js +1 -1
  193. package/dist/rag/chunkers/MarkdownChunker.d.ts +22 -0
  194. package/dist/rag/chunkers/MarkdownChunker.js +213 -9
  195. package/dist/rag/chunking/markdownChunker.d.ts +16 -0
  196. package/dist/rag/chunking/markdownChunker.js +174 -2
  197. package/dist/rag/pipeline/contextAssembly.js +2 -1
  198. package/dist/rag/ragIntegration.d.ts +18 -1
  199. package/dist/rag/ragIntegration.js +94 -14
  200. package/dist/rag/retrieval/vectorQueryTool.js +21 -4
  201. package/dist/server/abstract/baseServerAdapter.js +4 -1
  202. package/dist/server/adapters/fastifyAdapter.js +35 -30
  203. package/dist/services/server/ai/observability/instrumentation.d.ts +32 -0
  204. package/dist/services/server/ai/observability/instrumentation.js +39 -0
  205. package/dist/telemetry/attributes.d.ts +52 -0
  206. package/dist/telemetry/attributes.js +60 -0
  207. package/dist/telemetry/index.d.ts +3 -0
  208. package/dist/telemetry/index.js +3 -0
  209. package/dist/telemetry/telemetryService.d.ts +6 -0
  210. package/dist/telemetry/telemetryService.js +6 -0
  211. package/dist/telemetry/tracers.d.ts +15 -0
  212. package/dist/telemetry/tracers.js +16 -0
  213. package/dist/telemetry/withSpan.d.ts +9 -0
  214. package/dist/telemetry/withSpan.js +34 -0
  215. package/dist/types/contextTypes.d.ts +10 -0
  216. package/dist/types/errors.d.ts +62 -0
  217. package/dist/types/errors.js +107 -0
  218. package/dist/types/index.d.ts +2 -1
  219. package/dist/types/index.js +2 -0
  220. package/dist/types/providers.d.ts +107 -0
  221. package/dist/types/providers.js +69 -0
  222. package/dist/types/streamTypes.d.ts +14 -0
  223. package/dist/types/subscriptionTypes.d.ts +893 -0
  224. package/dist/types/subscriptionTypes.js +7 -0
  225. package/dist/utils/conversationMemory.js +121 -82
  226. package/dist/utils/logger.d.ts +5 -0
  227. package/dist/utils/logger.js +50 -2
  228. package/dist/utils/messageBuilder.js +22 -42
  229. package/dist/utils/modelDetection.js +3 -3
  230. package/dist/utils/providerConfig.d.ts +167 -0
  231. package/dist/utils/providerConfig.js +619 -9
  232. package/dist/utils/providerRetry.d.ts +41 -0
  233. package/dist/utils/providerRetry.js +113 -0
  234. package/dist/utils/retryability.d.ts +14 -0
  235. package/dist/utils/retryability.js +22 -0
  236. package/dist/utils/sanitizers/svg.js +4 -5
  237. package/dist/utils/tokenEstimation.d.ts +11 -1
  238. package/dist/utils/tokenEstimation.js +19 -4
  239. package/dist/utils/videoAnalysisProcessor.js +7 -3
  240. package/dist/workflow/config.d.ts +26 -26
  241. package/package.json +2 -1
@@ -1,5 +1,6 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
2
  import { streamText } from "ai";
3
+ import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api";
3
4
  import { AIProviderName } from "../constants/enums.js";
4
5
  import { BaseProvider } from "../core/baseProvider.js";
5
6
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
@@ -7,6 +8,7 @@ import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
7
8
  import { createProxyFetch } from "../proxy/proxyFetch.js";
8
9
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/errors.js";
9
10
  import { logger } from "../utils/logger.js";
11
+ import { calculateCost } from "../utils/pricing.js";
10
12
  import { createOpenAIConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
11
13
  import { isZodSchema } from "../utils/schemaConversion.js";
12
14
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
@@ -17,6 +19,7 @@ const getOpenAIApiKey = () => {
17
19
  const getOpenAIModel = () => {
18
20
  return getProviderModel("OPENAI_MODEL", "gpt-4o");
19
21
  };
22
+ const streamTracer = trace.getTracer("neurolink.provider.openai");
20
23
  /**
21
24
  * OpenAI Provider v2 - BaseProvider Implementation
22
25
  * Migrated to use factory pattern with exact Google AI provider pattern
@@ -274,27 +277,82 @@ export class OpenAIProvider extends BaseProvider {
274
277
  : "no-tools",
275
278
  });
276
279
  const model = await this.getAISDKModelWithMiddleware(options); // This is where network connection happens!
277
- const result = await streamText({
278
- model,
279
- messages: messages,
280
- temperature: options.temperature,
281
- maxTokens: options.maxTokens, // No default limit - unlimited unless specified
282
- tools,
283
- maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
284
- toolChoice: shouldUseTools && Object.keys(tools).length > 0 ? "auto" : "none",
285
- abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
286
- experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
287
- onStepFinish: ({ toolCalls, toolResults }) => {
288
- logger.info("Tool execution completed", { toolResults, toolCalls });
289
- // Handle tool execution storage
290
- this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
291
- logger.warn("[OpenAIProvider] Failed to store tool executions", {
292
- provider: this.providerName,
293
- error: error instanceof Error ? error.message : String(error),
294
- });
295
- });
280
+ // Wrap streamText in an OTel span to capture provider-level latency and token usage
281
+ const streamSpan = streamTracer.startSpan("neurolink.provider.streamText", {
282
+ kind: SpanKind.CLIENT,
283
+ attributes: {
284
+ "gen_ai.system": "openai",
285
+ "gen_ai.request.model": model.modelId || this.modelName || "unknown",
296
286
  },
297
287
  });
288
+ let result;
289
+ try {
290
+ result = streamText({
291
+ model,
292
+ messages: messages,
293
+ temperature: options.temperature,
294
+ maxTokens: options.maxTokens, // No default limit - unlimited unless specified
295
+ maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
296
+ tools,
297
+ maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
298
+ toolChoice: shouldUseTools && Object.keys(tools).length > 0 ? "auto" : "none",
299
+ abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
300
+ experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
301
+ onStepFinish: ({ toolCalls, toolResults }) => {
302
+ logger.info("Tool execution completed", {
303
+ toolResults,
304
+ toolCalls,
305
+ });
306
+ // Handle tool execution storage
307
+ this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
308
+ logger.warn("[OpenAIProvider] Failed to store tool executions", {
309
+ provider: this.providerName,
310
+ error: error instanceof Error ? error.message : String(error),
311
+ });
312
+ });
313
+ },
314
+ });
315
+ }
316
+ catch (streamError) {
317
+ streamSpan.end();
318
+ throw streamError;
319
+ }
320
+ // Collect token usage and finish reason asynchronously when the stream completes,
321
+ // then end the span. This avoids blocking the stream consumer.
322
+ result.usage
323
+ .then((usage) => {
324
+ streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
325
+ streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
326
+ const cost = calculateCost(this.providerName, this.modelName, {
327
+ input: usage.promptTokens || 0,
328
+ output: usage.completionTokens || 0,
329
+ total: (usage.promptTokens || 0) + (usage.completionTokens || 0),
330
+ });
331
+ if (cost && cost > 0) {
332
+ streamSpan.setAttribute("neurolink.cost", cost);
333
+ }
334
+ })
335
+ .catch(() => {
336
+ // Usage may not be available if the stream is aborted
337
+ });
338
+ result.finishReason
339
+ .then((reason) => {
340
+ streamSpan.setAttribute("gen_ai.response.finish_reason", reason || "unknown");
341
+ })
342
+ .catch(() => {
343
+ // Finish reason may not be available if the stream is aborted
344
+ });
345
+ result.text
346
+ .then(() => {
347
+ streamSpan.end();
348
+ })
349
+ .catch((err) => {
350
+ streamSpan.setStatus({
351
+ code: SpanStatusCode.ERROR,
352
+ message: err instanceof Error ? err.message : String(err),
353
+ });
354
+ streamSpan.end();
355
+ });
298
356
  timeoutController?.cleanup();
299
357
  // Debug the actual result structure
300
358
  logger.debug(`OpenAI: streamText result structure:`, {
@@ -9,6 +9,7 @@ import { createStructuredOutputParser, isStructuredContent, } from "./structured
9
9
  import { SageMakerError } from "./errors.js";
10
10
  import { logger } from "../../utils/logger.js";
11
11
  import { randomUUID } from "crypto";
12
+ import { estimateTokens } from "../../utils/tokenEstimation.js";
12
13
  /**
13
14
  * Constants for JSON parsing and validation
14
15
  */
@@ -623,9 +624,8 @@ function extractApiErrorMessage(errorData) {
623
624
  * Utility function to estimate token usage when not provided
624
625
  */
625
626
  export function estimateTokenUsage(prompt, completion) {
626
- // Rough estimation: ~4 characters per token for English text
627
- const promptTokens = Math.ceil(prompt.length / 4);
628
- const completionTokens = Math.ceil(completion.length / 4);
627
+ const promptTokens = estimateTokens(prompt, "sagemaker");
628
+ const completionTokens = estimateTokens(completion, "sagemaker");
629
629
  return {
630
630
  promptTokens,
631
631
  completionTokens,
@@ -7,6 +7,7 @@
7
7
  import { ReadableStream } from "stream/web";
8
8
  import { handleSageMakerError, SageMakerError } from "./errors.js";
9
9
  import { logger } from "../../utils/logger.js";
10
+ import { estimateTokens } from "../../utils/tokenEstimation.js";
10
11
  import { createSageMakerDetector, } from "./detection.js";
11
12
  import { StreamingParserFactory } from "./parsers.js";
12
13
  /**
@@ -320,9 +321,8 @@ export async function createSyntheticStream(text, usage, options = {}) {
320
321
  * @returns Estimated usage information
321
322
  */
322
323
  export function estimateTokenUsage(prompt, completion) {
323
- // Simple estimation: ~4 characters per token (rough average for English)
324
- const promptTokens = Math.ceil(prompt.length / 4);
325
- const completionTokens = Math.ceil(completion.length / 4);
324
+ const promptTokens = estimateTokens(prompt, "sagemaker");
325
+ const completionTokens = estimateTokens(completion, "sagemaker");
326
326
  return {
327
327
  promptTokens,
328
328
  completionTokens,
@@ -4,31 +4,84 @@
4
4
  * Lightweight implementation extracted from research of major proxy packages
5
5
  */
6
6
  import { logger } from "../utils/logger.js";
7
+ import { SpanStatusCode } from "@opentelemetry/api";
8
+ import { tracers } from "../telemetry/tracers.js";
7
9
  import { shouldBypassProxy } from "./utils/noProxyUtils.js";
10
+ const fetchTracer = tracers.http;
11
+ /**
12
+ * Extract hostname from a URL string for safe logging (no auth tokens or paths).
13
+ * Returns "[unknown]" if parsing fails.
14
+ */
15
+ function extractHostname(url) {
16
+ try {
17
+ const urlStr = typeof url === "string"
18
+ ? url
19
+ : url instanceof URL
20
+ ? url.href
21
+ : url.url;
22
+ const parsed = new URL(urlStr);
23
+ return parsed.hostname;
24
+ }
25
+ catch {
26
+ return "[unknown]";
27
+ }
28
+ }
8
29
  /**
9
30
  * Retry-aware fetch wrapper for transient network errors (ECONNRESET, ETIMEDOUT, socket hang up).
10
31
  * Protects all LLM API calls and token refreshes that go through createProxyFetch().
32
+ * Instrumented with OpenTelemetry spans for retry visibility.
11
33
  */
12
34
  async function fetchWithRetry(url, init, maxRetries = 3, baseDelay = 500) {
13
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
35
+ const hostname = extractHostname(url);
36
+ return fetchTracer.startActiveSpan("neurolink.http.fetchWithRetry", async (span) => {
37
+ span.setAttribute("http.request.max_retries", maxRetries);
38
+ span.setAttribute("http.request.hostname", hostname);
39
+ span.setAttribute("http.request.method", init?.method || "GET");
40
+ let totalAttempts = 0;
14
41
  try {
15
- return await fetch(url, init);
16
- }
17
- catch (error) {
18
- const err = error;
19
- const isRetryable = err?.code === "ECONNRESET" ||
20
- err?.code === "ETIMEDOUT" ||
21
- err?.message?.includes("socket hang up") ||
22
- err?.message?.includes("network socket disconnected");
23
- if (!isRetryable || attempt === maxRetries) {
24
- throw error;
42
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
43
+ totalAttempts = attempt + 1;
44
+ try {
45
+ const response = await fetch(url, init);
46
+ // Record success attributes
47
+ span.setAttribute("http.request.total_attempts", totalAttempts);
48
+ span.setAttribute("http.response.status_code", response.status);
49
+ span.setStatus({ code: SpanStatusCode.OK });
50
+ return response;
51
+ }
52
+ catch (error) {
53
+ const err = error;
54
+ const isRetryable = err?.code === "ECONNRESET" ||
55
+ err?.code === "ETIMEDOUT" ||
56
+ err?.message?.includes("socket hang up") ||
57
+ err?.message?.includes("network socket disconnected");
58
+ if (!isRetryable || attempt === maxRetries) {
59
+ // Final failure — record on span and rethrow
60
+ span.setAttribute("http.request.total_attempts", totalAttempts);
61
+ span.setStatus({
62
+ code: SpanStatusCode.ERROR,
63
+ message: err?.message || err?.code || "fetchWithRetry final failure",
64
+ });
65
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
66
+ throw error;
67
+ }
68
+ // Transient error — add retry event and continue loop
69
+ const delay = baseDelay * Math.pow(2, attempt);
70
+ span.addEvent("http.request.retry", {
71
+ "retry.attempt": attempt + 1,
72
+ "retry.delay_ms": delay,
73
+ "retry.error": (err?.code || err?.message || String(error)).slice(0, 256),
74
+ });
75
+ logger.debug(`[fetchWithRetry] Transient error (${err?.code || err?.message}), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
76
+ await new Promise((r) => setTimeout(r, delay));
77
+ }
25
78
  }
26
- const delay = baseDelay * Math.pow(2, attempt);
27
- logger.debug(`[fetchWithRetry] Transient error (${err?.code || err?.message}), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
28
- await new Promise((r) => setTimeout(r, delay));
79
+ throw new Error("fetchWithRetry exhausted"); // unreachable
29
80
  }
30
- }
31
- throw new Error("fetchWithRetry exhausted"); // unreachable
81
+ finally {
82
+ span.end();
83
+ }
84
+ });
32
85
  }
33
86
  /**
34
87
  * Parse request body to readable format for debug logging
@@ -69,22 +122,6 @@ const SENSITIVE_HEADERS = new Set([
69
122
  "cookie",
70
123
  "set-cookie",
71
124
  ]);
72
- /**
73
- * Extract all headers as plain object with sensitive values redacted
74
- */
75
- function getAllHeaders(headers) {
76
- if (!headers) {
77
- return {};
78
- }
79
- const entries = headers instanceof Headers
80
- ? [...headers.entries()]
81
- : Array.isArray(headers)
82
- ? headers
83
- : Object.entries(headers);
84
- return Object.fromEntries(entries.map(([key, value]) => SENSITIVE_HEADERS.has(key.toLowerCase())
85
- ? [key, `${value.substring(0, 4)}***`]
86
- : [key, value]));
87
- }
88
125
  /**
89
126
  * Clone response and read body + headers for debug logging
90
127
  */
@@ -287,13 +324,11 @@ export function createProxyFetch() {
287
324
  ? input.href
288
325
  : input.url;
289
326
  if (logger.shouldLog("debug")) {
290
- const { parsed: requestBody, size: bodySize, type: bodyType, } = parseBody(init?.body);
327
+ const { size: bodySize, type: bodyType } = parseBody(init?.body);
291
328
  logger.debug("[Observability] HTTP request to LLM provider", {
292
329
  requestId: reqId,
293
330
  url,
294
331
  method: init?.method || "POST",
295
- headers: getAllHeaders(init?.headers),
296
- body: requestBody,
297
332
  bodySize,
298
333
  bodyType,
299
334
  });
@@ -308,10 +343,10 @@ export function createProxyFetch() {
308
343
  status: response.status,
309
344
  statusText: response.statusText,
310
345
  durationMs: Date.now() - startTs,
311
- headers: responseHeaders,
312
- body: responseBody,
313
- bodySize: responseSize,
346
+ contentLength: responseSize,
347
+ hasContent: !!responseBody,
314
348
  bodyType: responseType,
349
+ responseHeaders,
315
350
  });
316
351
  }
317
352
  return response;
@@ -345,13 +380,11 @@ export function createProxyFetch() {
345
380
  : input.url;
346
381
  // Request logging with sensitive header redaction — gated behind debug check
347
382
  if (logger.shouldLog("debug")) {
348
- const { parsed: requestBody, size: bodySize, type: bodyType, } = parseBody(init?.body);
383
+ const { size: bodySize, type: bodyType } = parseBody(init?.body);
349
384
  logger.debug("[Observability] HTTP request to LLM provider", {
350
385
  requestId,
351
386
  url: targetUrl,
352
387
  method: init?.method || "POST",
353
- headers: getAllHeaders(init?.headers),
354
- body: requestBody,
355
388
  bodySize,
356
389
  bodyType,
357
390
  });
@@ -392,7 +425,7 @@ export function createProxyFetch() {
392
425
  // Create/reuse proxy agent (HTTP/HTTPS/SOCKS)
393
426
  const agentCache = globalThis.__NL_PROXY_AGENT_CACHE__ ??
394
427
  (globalThis.__NL_PROXY_AGENT_CACHE__ = new Map());
395
- const cacheKey = maskProxyUrl(proxyUrl) ?? proxyUrl; // credentials stripped for key
428
+ const cacheKey = maskProxyUrl(proxyUrl) ?? proxyUrl; // mask credentials in cache key
396
429
  const dispatcher = agentCache.get(cacheKey) || (await createProxyAgent(proxyUrl));
397
430
  agentCache.set(cacheKey, dispatcher);
398
431
  logger.debug(`[Proxy Fetch] ✅ ENHANCED PROXY AGENT CREATED`, {
@@ -431,11 +464,11 @@ export function createProxyFetch() {
431
464
  status: response?.status,
432
465
  statusText: response?.statusText,
433
466
  durationMs: Date.now() - requestStartTime,
434
- headers: responseHeaders,
435
- body: responseBody,
436
- bodySize: responseSize,
467
+ contentLength: responseSize,
468
+ hasContent: !!responseBody,
437
469
  bodyType: responseType,
438
470
  proxied: true,
471
+ responseHeaders,
439
472
  });
440
473
  }
441
474
  logger.debug(`[Proxy Fetch] ENHANCED PROXY SUCCESS`, {
@@ -481,11 +514,11 @@ export function createProxyFetch() {
481
514
  status: response.status,
482
515
  statusText: response.statusText,
483
516
  durationMs: Date.now() - requestStartTime,
484
- headers: responseHeaders,
485
- body: responseBody,
486
- bodySize: responseSize,
517
+ contentLength: responseSize,
518
+ hasContent: !!responseBody,
487
519
  bodyType: responseType,
488
520
  proxied: false,
521
+ responseHeaders,
489
522
  });
490
523
  }
491
524
  return response;
@@ -50,7 +50,7 @@ const DEFAULT_CHUNKER_METADATA = {
50
50
  },
51
51
  markdown: {
52
52
  description: "Splits markdown content by headers and structural elements",
53
- defaultConfig: { maxSize: 1000, overlap: 0 },
53
+ defaultConfig: { maxSize: 1000, overlap: 50 },
54
54
  supportedOptions: ["maxSize", "headerLevels", "splitCodeBlocks"],
55
55
  useCases: ["Documentation processing", "README files"],
56
56
  aliases: ["md", "markdown-header"],
@@ -2,6 +2,8 @@
2
2
  * Markdown Chunker
3
3
  *
4
4
  * Splits markdown content by headers and structural elements.
5
+ * Preserves markdown tables by detecting table boundaries and splitting
6
+ * on row boundaries when a table exceeds the max chunk size.
5
7
  */
6
8
  import type { Chunk, ChunkerConfig, ChunkingStrategy } from "../types.js";
7
9
  import { BaseChunker } from "./BaseChunker.js";
@@ -12,4 +14,24 @@ export declare class MarkdownChunker extends BaseChunker {
12
14
  readonly strategy: ChunkingStrategy;
13
15
  getDefaultConfig(): ChunkerConfig;
14
16
  protected doChunk(content: string, config: ChunkerConfig): Promise<Chunk[]>;
17
+ /**
18
+ * Split content while preserving markdown tables.
19
+ *
20
+ * Strategy:
21
+ * 1. Identify table blocks in the content.
22
+ * 2. Split content into segments: non-table text and table blocks.
23
+ * 3. Non-table text is split using paragraph/sentence boundaries (existing logic).
24
+ * 4. Tables that fit in a chunk are kept intact.
25
+ * 5. Oversized tables are split on row boundaries, repeating the header row.
26
+ */
27
+ private splitContentTableAware;
28
+ /**
29
+ * Split a table on row boundaries, repeating header + separator in each chunk.
30
+ */
31
+ private splitTableByRows;
32
+ /**
33
+ * Split non-table text using paragraph and sentence boundaries.
34
+ * This is the original splitContent logic extracted for reuse.
35
+ */
36
+ private splitPlainContent;
15
37
  }
@@ -2,8 +2,42 @@
2
2
  * Markdown Chunker
3
3
  *
4
4
  * Splits markdown content by headers and structural elements.
5
+ * Preserves markdown tables by detecting table boundaries and splitting
6
+ * on row boundaries when a table exceeds the max chunk size.
5
7
  */
6
8
  import { BaseChunker, DEFAULT_CHUNKER_CONFIG } from "./BaseChunker.js";
9
+ /** Matches a markdown table separator row like |---|---| or |:--:|---:| */
10
+ const TABLE_SEPARATOR_RE = /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|?\s*$/;
11
+ /** Matches a line that looks like a table row (starts with |) */
12
+ const TABLE_ROW_RE = /^\|.+\|?\s*$/;
13
+ /**
14
+ * Detect contiguous table blocks in text.
15
+ * Returns an array of { start, end } line index ranges (inclusive).
16
+ * A table is a sequence of lines where the second line is a separator.
17
+ */
18
+ function detectTableRanges(lines) {
19
+ const ranges = [];
20
+ let i = 0;
21
+ while (i < lines.length) {
22
+ // A table needs at least a header row + separator
23
+ if (i + 1 < lines.length &&
24
+ TABLE_ROW_RE.test(lines[i]) &&
25
+ TABLE_SEPARATOR_RE.test(lines[i + 1])) {
26
+ const start = i;
27
+ // Advance past header + separator
28
+ i += 2;
29
+ // Consume remaining data rows
30
+ while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
31
+ i++;
32
+ }
33
+ ranges.push({ start, end: i - 1 });
34
+ }
35
+ else {
36
+ i++;
37
+ }
38
+ }
39
+ return ranges;
40
+ }
7
41
  /**
8
42
  * Markdown Chunker
9
43
  */
@@ -13,7 +47,7 @@ export class MarkdownChunker extends BaseChunker {
13
47
  return {
14
48
  ...DEFAULT_CHUNKER_CONFIG,
15
49
  maxSize: 1000,
16
- overlap: 0,
50
+ overlap: 50,
17
51
  };
18
52
  }
19
53
  async doChunk(content, config) {
@@ -74,16 +108,14 @@ export class MarkdownChunker extends BaseChunker {
74
108
  if (!fullContent) {
75
109
  continue;
76
110
  }
77
- // Split if too large
111
+ // Split if too large — use table-aware splitting
78
112
  if (fullContent.length > maxSize) {
79
- const subChunks = this.splitBySizeWithOverlap(fullContent, maxSize, 0);
113
+ const subChunks = this.splitContentTableAware(fullContent, maxSize);
80
114
  for (const sub of subChunks) {
81
- const startOffset = content.indexOf(sub.text, offset);
82
- chunks.push(this.createChunk(sub.text, chunks.length, startOffset >= 0 ? startOffset : offset, startOffset >= 0
83
- ? startOffset + sub.text.length
84
- : offset + sub.text.length, "unknown", { sectionContext: section.header }));
115
+ const startOffset = content.indexOf(sub, offset);
116
+ chunks.push(this.createChunk(sub, chunks.length, startOffset >= 0 ? startOffset : offset, startOffset >= 0 ? startOffset + sub.length : offset + sub.length, "unknown", { sectionContext: section.header }));
85
117
  if (startOffset >= 0) {
86
- offset = startOffset + 1;
118
+ offset = startOffset + sub.length;
87
119
  }
88
120
  }
89
121
  }
@@ -93,9 +125,181 @@ export class MarkdownChunker extends BaseChunker {
93
125
  ? startOffset + fullContent.length
94
126
  : offset + fullContent.length, "unknown", { sectionContext: section.header }));
95
127
  if (startOffset >= 0) {
96
- offset = startOffset + 1;
128
+ offset = startOffset + fullContent.length;
129
+ }
130
+ }
131
+ }
132
+ return chunks;
133
+ }
134
+ /**
135
+ * Split content while preserving markdown tables.
136
+ *
137
+ * Strategy:
138
+ * 1. Identify table blocks in the content.
139
+ * 2. Split content into segments: non-table text and table blocks.
140
+ * 3. Non-table text is split using paragraph/sentence boundaries (existing logic).
141
+ * 4. Tables that fit in a chunk are kept intact.
142
+ * 5. Oversized tables are split on row boundaries, repeating the header row.
143
+ */
144
+ splitContentTableAware(content, maxSize) {
145
+ const lines = content.split("\n");
146
+ const tableRanges = detectTableRanges(lines);
147
+ // If no tables, fall back to existing splitting logic
148
+ if (tableRanges.length === 0) {
149
+ return this.splitPlainContent(content, maxSize, this.config.overlap ?? 0);
150
+ }
151
+ // Build segments: alternating non-table and table blocks
152
+ const segments = [];
153
+ let lineIdx = 0;
154
+ for (const range of tableRanges) {
155
+ // Non-table text before this table
156
+ if (lineIdx < range.start) {
157
+ const text = lines.slice(lineIdx, range.start).join("\n").trim();
158
+ if (text) {
159
+ segments.push({ text, isTable: false });
160
+ }
161
+ }
162
+ // The table itself
163
+ const tableText = lines.slice(range.start, range.end + 1).join("\n");
164
+ segments.push({ text: tableText, isTable: true });
165
+ lineIdx = range.end + 1;
166
+ }
167
+ // Trailing non-table text
168
+ if (lineIdx < lines.length) {
169
+ const text = lines.slice(lineIdx).join("\n").trim();
170
+ if (text) {
171
+ segments.push({ text, isTable: false });
172
+ }
173
+ }
174
+ // Now produce chunks, trying to pack segments together up to maxSize
175
+ const result = [];
176
+ let current = "";
177
+ for (const seg of segments) {
178
+ if (!seg.isTable) {
179
+ // Non-table text: try to append, split if needed
180
+ const pieces = this.splitPlainContent(seg.text, maxSize, this.config.overlap ?? 0);
181
+ for (const piece of pieces) {
182
+ if (current.length === 0) {
183
+ current = piece;
184
+ }
185
+ else if (current.length + 1 + piece.length <= maxSize) {
186
+ current += "\n" + piece;
187
+ }
188
+ else {
189
+ result.push(current);
190
+ current = piece;
191
+ }
192
+ }
193
+ }
194
+ else {
195
+ // Table block
196
+ if (seg.text.length <= maxSize) {
197
+ // Table fits — try to append to current chunk
198
+ if (current.length === 0) {
199
+ current = seg.text;
200
+ }
201
+ else if (current.length + 2 + seg.text.length <= maxSize) {
202
+ current += "\n\n" + seg.text;
203
+ }
204
+ else {
205
+ result.push(current);
206
+ current = seg.text;
207
+ }
208
+ }
209
+ else {
210
+ // Oversized table — flush current, then split table on row boundaries
211
+ if (current) {
212
+ result.push(current);
213
+ current = "";
214
+ }
215
+ const tableChunks = this.splitTableByRows(seg.text, maxSize);
216
+ result.push(...tableChunks);
217
+ }
218
+ }
219
+ }
220
+ if (current) {
221
+ result.push(current);
222
+ }
223
+ return result.length > 0 ? result : [content];
224
+ }
225
+ /**
226
+ * Split a table on row boundaries, repeating header + separator in each chunk.
227
+ */
228
+ splitTableByRows(tableText, maxSize) {
229
+ const rows = tableText.split("\n");
230
+ if (rows.length < 3) {
231
+ // Not a proper table (need header + separator + at least 1 data row)
232
+ return [tableText];
233
+ }
234
+ const headerRow = rows[0];
235
+ const separatorRow = rows[1];
236
+ const headerBlock = headerRow + "\n" + separatorRow;
237
+ const dataRows = rows.slice(2);
238
+ // If even the header doesn't fit, fall back to size-based split
239
+ if (headerBlock.length > maxSize) {
240
+ return this.splitPlainContent(tableText, maxSize, this.config.overlap ?? 0);
241
+ }
242
+ const chunks = [];
243
+ let currentChunk = headerBlock;
244
+ for (const row of dataRows) {
245
+ // Guard: single row exceeds budget — flush and emit as standalone chunk
246
+ const singleRowChunk = `${headerBlock}\n${row}`;
247
+ if (singleRowChunk.length > maxSize) {
248
+ if (currentChunk.length > headerBlock.length) {
249
+ chunks.push(currentChunk);
250
+ }
251
+ chunks.push(singleRowChunk);
252
+ currentChunk = headerBlock;
253
+ continue;
254
+ }
255
+ const candidate = currentChunk + "\n" + row;
256
+ if (candidate.length <= maxSize) {
257
+ currentChunk = candidate;
258
+ }
259
+ else {
260
+ // Flush current chunk (skip if it only contains the header)
261
+ if (currentChunk.length > headerBlock.length) {
262
+ chunks.push(currentChunk);
263
+ }
264
+ // Start new chunk with header repeated
265
+ currentChunk = headerBlock + "\n" + row;
266
+ }
267
+ }
268
+ if (currentChunk.length > headerBlock.length) {
269
+ chunks.push(currentChunk);
270
+ }
271
+ return chunks.length > 0 ? chunks : [tableText];
272
+ }
273
+ /**
274
+ * Split non-table text using paragraph and sentence boundaries.
275
+ * This is the original splitContent logic extracted for reuse.
276
+ */
277
+ splitPlainContent(content, maxSize, overlap = 0) {
278
+ if (content.length <= maxSize) {
279
+ return [content];
280
+ }
281
+ const chunks = [];
282
+ let start = 0;
283
+ while (start < content.length) {
284
+ let end = Math.min(start + maxSize, content.length);
285
+ if (end < content.length) {
286
+ const searchStart = Math.max(start, end - 200);
287
+ const searchText = content.slice(searchStart, end);
288
+ // Look for paragraph break first
289
+ const paragraphBreak = searchText.lastIndexOf("\n\n");
290
+ if (paragraphBreak > 0) {
291
+ end = searchStart + paragraphBreak;
292
+ }
293
+ else {
294
+ // Look for sentence break
295
+ const sentenceBreak = searchText.search(/[.!?]\s+[A-Z]/);
296
+ if (sentenceBreak > 0) {
297
+ end = searchStart + sentenceBreak + 1;
298
+ }
97
299
  }
98
300
  }
301
+ chunks.push(content.slice(start, end));
302
+ start = Math.max(start + 1, end - overlap);
99
303
  }
100
304
  return chunks;
101
305
  }