@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
@@ -6,7 +6,9 @@ import { createProxyFetch } from "../proxy/proxyFetch.js";
6
6
  import { logger } from "../utils/logger.js";
7
7
  import { buildMultimodalMessagesArray } from "../utils/messageBuilder.js";
8
8
  import { buildMultimodalOptions } from "../utils/multimodalOptionsBuilder.js";
9
+ import { estimateTokens } from "../utils/tokenEstimation.js";
9
10
  import { InvalidModelError, NetworkError, ProviderError, } from "../types/errors.js";
11
+ import { tracers, ATTR, withClientSpan } from "../telemetry/index.js";
10
12
  import { TimeoutError } from "../utils/timeout.js";
11
13
  // Model version constants (configurable via environment)
12
14
  const DEFAULT_OLLAMA_MODEL = "llama3.1:8b";
@@ -55,8 +57,8 @@ class OllamaLanguageModel {
55
57
  this.baseUrl = baseUrl;
56
58
  this.timeout = timeout;
57
59
  }
58
- estimateTokens(text) {
59
- return Math.ceil(text.length / 4);
60
+ estimateTokenCount(text) {
61
+ return estimateTokens(text, "ollama");
60
62
  }
61
63
  convertMessagesToPrompt(messages) {
62
64
  return messages
@@ -87,7 +89,9 @@ class OllamaLanguageModel {
87
89
  max_tokens: options.maxTokens,
88
90
  stream: false,
89
91
  };
90
- logger.debug("[OllamaLanguageModel] Using OpenAI-compatible API with messages:", JSON.stringify(messages, null, 2));
92
+ if (logger.shouldLog("debug")) {
93
+ logger.debug("[OllamaLanguageModel] Using OpenAI-compatible API with messages:", JSON.stringify(messages, null, 2));
94
+ }
91
95
  const response = await proxyFetch(`${this.baseUrl}/v1/chat/completions`, {
92
96
  method: "POST",
93
97
  headers: { "Content-Type": "application/json" },
@@ -105,8 +109,8 @@ class OllamaLanguageModel {
105
109
  text,
106
110
  usage: {
107
111
  promptTokens: usage.prompt_tokens ??
108
- this.estimateTokens(JSON.stringify(messages)),
109
- completionTokens: usage.completion_tokens ?? this.estimateTokens(text),
112
+ this.estimateTokenCount(JSON.stringify(messages)),
113
+ completionTokens: usage.completion_tokens ?? this.estimateTokenCount(text),
110
114
  totalTokens: usage.total_tokens,
111
115
  },
112
116
  finishReason: "stop",
@@ -150,11 +154,12 @@ class OllamaLanguageModel {
150
154
  return {
151
155
  text: data.response,
152
156
  usage: {
153
- promptTokens: data.prompt_eval_count ?? this.estimateTokens(prompt),
154
- completionTokens: data.eval_count ?? this.estimateTokens(String(data.response ?? "")),
155
- totalTokens: (data.prompt_eval_count ?? this.estimateTokens(prompt)) +
157
+ promptTokens: data.prompt_eval_count ?? this.estimateTokenCount(prompt),
158
+ completionTokens: data.eval_count ??
159
+ this.estimateTokenCount(String(data.response ?? "")),
160
+ totalTokens: (data.prompt_eval_count ?? this.estimateTokenCount(prompt)) +
156
161
  (data.eval_count ??
157
- this.estimateTokens(String(data.response ?? ""))),
162
+ this.estimateTokenCount(String(data.response ?? ""))),
158
163
  },
159
164
  finishReason: "stop",
160
165
  rawCall: {
@@ -329,7 +334,7 @@ class OllamaLanguageModel {
329
334
  finishReason: "stop",
330
335
  usage: {
331
336
  promptTokens: data.prompt_eval_count ||
332
- this.estimateTokens(data.context || ""),
337
+ this.estimateTokenCount(data.context || ""),
333
338
  completionTokens: data.eval_count || 0,
334
339
  },
335
340
  };
@@ -357,8 +362,11 @@ class OllamaLanguageModel {
357
362
  const decoder = new TextDecoder();
358
363
  let buffer = "";
359
364
  // Estimate prompt tokens from messages (matches non-streaming behavior)
360
- const totalPromptTokens = this.estimateTokens(JSON.stringify(messages));
361
- let totalCompletionTokens = 0;
365
+ const totalPromptTokens = this.estimateTokenCount(JSON.stringify(messages));
366
+ // Accumulate full completion text; estimate tokens once at the end to avoid
367
+ // per-chunk rounding inflation that occurs when estimateTokenCount is called
368
+ // on every delta and the results are summed.
369
+ let completionText = "";
362
370
  try {
363
371
  while (true) {
364
372
  const { done, value } = await reader.read();
@@ -384,14 +392,16 @@ class OllamaLanguageModel {
384
392
  type: "text-delta",
385
393
  textDelta: content,
386
394
  };
387
- totalCompletionTokens += this.estimateTokens(content);
395
+ completionText += content;
388
396
  }
389
397
  // Check for finish
390
398
  const finishReason = data.choices?.[0]?.finish_reason;
391
399
  if (finishReason === "stop") {
392
- // Extract usage if available and update tokens
400
+ // Prefer server-reported usage; fall back to a single estimate over
401
+ // the full accumulated text (avoids per-chunk rounding inflation).
393
402
  const promptTokens = data.usage?.prompt_tokens || totalPromptTokens;
394
- const completionTokens = data.usage?.completion_tokens || totalCompletionTokens;
403
+ const completionTokens = data.usage?.completion_tokens ||
404
+ this.estimateTokenCount(completionText);
395
405
  yield {
396
406
  type: "finish",
397
407
  finishReason: "stop",
@@ -418,7 +428,7 @@ class OllamaLanguageModel {
418
428
  finishReason: "stop",
419
429
  usage: {
420
430
  promptTokens: totalPromptTokens,
421
- completionTokens: totalCompletionTokens,
431
+ completionTokens: this.estimateTokenCount(completionText),
422
432
  },
423
433
  };
424
434
  }
@@ -604,289 +614,330 @@ export class OllamaProvider extends BaseProvider {
604
614
  * Uses conversation loop to handle multi-step tool execution
605
615
  */
606
616
  async executeStreamWithTools(options, _analysisSchema) {
607
- const startTime = Date.now();
608
- const maxIterations = options.maxSteps || DEFAULT_MAX_STEPS;
609
- let iteration = 0;
610
- // Get all available tools (direct + MCP + external)
611
- // BaseProvider.stream() pre-merges base tools + external tools into options.tools
612
- const allTools = options.tools ||
613
- (await this.getAllTools());
614
- // Convert tools to Ollama format
615
- const ollamaTools = this.convertToolsToOllamaFormat(allTools);
616
- // Validate that PDFs are not provided
617
- if (options.input?.pdfFiles && options.input.pdfFiles.length > 0) {
618
- throw new Error("PDF inputs are not supported by OllamaProvider. " +
619
- "Please remove PDFs or use a supported provider (OpenAI, Anthropic, Google Vertex AI, etc.).");
620
- }
621
- // Initialize conversation history
622
- const conversationHistory = [];
623
- // Build initial messages
624
- const hasMultimodalInput = !!(options.input?.images?.length ||
625
- options.input?.content?.length ||
626
- options.input?.files?.length ||
627
- options.input?.csvFiles?.length);
628
- if (hasMultimodalInput) {
629
- logger.debug(`Ollama: Detected multimodal input, using multimodal message builder`, {
630
- hasImages: !!options.input?.images?.length,
631
- imageCount: options.input?.images?.length || 0,
632
- });
633
- const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
634
- const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
635
- conversationHistory.push(...this.convertToOllamaMessages(multimodalMessages));
636
- }
637
- else {
638
- if (options.systemPrompt) {
617
+ return withClientSpan({
618
+ name: "neurolink.provider.stream",
619
+ tracer: tracers.provider,
620
+ attributes: {
621
+ [ATTR.GEN_AI_SYSTEM]: "ollama",
622
+ [ATTR.GEN_AI_MODEL]: this.modelName || FALLBACK_OLLAMA_MODEL,
623
+ [ATTR.GEN_AI_OPERATION]: "stream",
624
+ [ATTR.NL_HAS_TOOLS]: true,
625
+ [ATTR.NL_STREAM_MODE]: true,
626
+ },
627
+ }, async (span) => {
628
+ const startTime = Date.now();
629
+ const maxIterations = options.maxSteps || DEFAULT_MAX_STEPS;
630
+ let iteration = 0;
631
+ // Get all available tools (direct + MCP + external)
632
+ // BaseProvider.stream() pre-merges base tools + external tools into options.tools
633
+ const allTools = options.tools ||
634
+ (await this.getAllTools());
635
+ // Convert tools to Ollama format
636
+ const ollamaTools = this.convertToolsToOllamaFormat(allTools);
637
+ span.setAttribute(ATTR.NL_TOOL_COUNT, ollamaTools.length);
638
+ // Validate that PDFs are not provided
639
+ if (options.input?.pdfFiles && options.input.pdfFiles.length > 0) {
640
+ throw this.handleProviderError(new Error("PDF inputs are not supported by OllamaProvider. " +
641
+ "Please remove PDFs or use a supported provider (OpenAI, Anthropic, Google Vertex AI, etc.)."));
642
+ }
643
+ // Initialize conversation history
644
+ const conversationHistory = [];
645
+ // Build initial messages
646
+ const hasMultimodalInput = !!(options.input?.images?.length ||
647
+ options.input?.content?.length ||
648
+ options.input?.files?.length ||
649
+ options.input?.csvFiles?.length);
650
+ if (hasMultimodalInput) {
651
+ logger.debug(`Ollama: Detected multimodal input, using multimodal message builder`, {
652
+ hasImages: !!options.input?.images?.length,
653
+ imageCount: options.input?.images?.length || 0,
654
+ });
655
+ const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
656
+ const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
657
+ conversationHistory.push(...this.convertToOllamaMessages(multimodalMessages));
658
+ }
659
+ else {
660
+ if (options.systemPrompt) {
661
+ conversationHistory.push({
662
+ role: "system",
663
+ content: options.systemPrompt,
664
+ });
665
+ }
639
666
  conversationHistory.push({
640
- role: "system",
641
- content: options.systemPrompt,
667
+ role: "user",
668
+ content: options.input.text,
642
669
  });
643
670
  }
644
- conversationHistory.push({
645
- role: "user",
646
- content: options.input.text,
647
- });
648
- }
649
- // Conversation loop for multi-step tool execution
650
- const stream = new ReadableStream({
651
- start: async (controller) => {
652
- try {
653
- while (iteration < maxIterations) {
654
- logger.debug(`[OllamaProvider] Conversation iteration ${iteration + 1}/${maxIterations}`);
655
- // Make API request
656
- const response = await proxyFetch(`${this.baseUrl}/v1/chat/completions`, {
657
- method: "POST",
658
- headers: { "Content-Type": "application/json" },
659
- body: JSON.stringify({
660
- model: this.modelName || FALLBACK_OLLAMA_MODEL,
661
- messages: conversationHistory,
662
- tools: ollamaTools,
663
- tool_choice: "auto",
664
- stream: true,
665
- temperature: options.temperature,
666
- max_tokens: options.maxTokens,
667
- }),
668
- signal: createAbortSignalWithTimeout(this.timeout),
669
- });
670
- if (!response.ok) {
671
- throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
672
- }
673
- // Process response stream
674
- const { content, toolCalls, finishReason } = await this.processOllamaResponse(response, controller);
675
- // Add assistant message to history
676
- const assistantMessage = {
677
- role: "assistant",
678
- content: content || "",
679
- };
680
- if (toolCalls && toolCalls.length > 0) {
681
- assistantMessage.tool_calls = toolCalls;
682
- }
683
- conversationHistory.push(assistantMessage);
684
- // Check finish reason
685
- if (finishReason === "stop" || !finishReason) {
686
- // Conversation complete
687
- controller.close();
688
- break;
689
- }
690
- else if (finishReason === "tool_calls" &&
691
- toolCalls &&
692
- toolCalls.length > 0) {
693
- // Execute tools
694
- logger.debug(`[OllamaProvider] Executing ${toolCalls.length} tools`);
695
- const toolResults = await this.executeOllamaTools(toolCalls, options);
696
- // Add tool results to conversation
697
- const toolMessage = {
698
- role: "tool",
699
- content: JSON.stringify(toolResults),
700
- };
701
- conversationHistory.push(toolMessage);
702
- iteration++;
703
- }
704
- else if (finishReason === "length") {
705
- // Max tokens reached, continue conversation
706
- logger.debug(`[OllamaProvider] Max tokens reached, continuing`);
707
- conversationHistory.push({
708
- role: "user",
709
- content: "Please continue.",
671
+ // Conversation loop for multi-step tool execution
672
+ const stream = new ReadableStream({
673
+ start: async (controller) => {
674
+ try {
675
+ while (iteration < maxIterations) {
676
+ logger.debug(`[OllamaProvider] Conversation iteration ${iteration + 1}/${maxIterations}`);
677
+ // Make API request
678
+ const response = await proxyFetch(`${this.baseUrl}/v1/chat/completions`, {
679
+ method: "POST",
680
+ headers: { "Content-Type": "application/json" },
681
+ body: JSON.stringify({
682
+ model: this.modelName || FALLBACK_OLLAMA_MODEL,
683
+ messages: conversationHistory,
684
+ tools: ollamaTools,
685
+ tool_choice: "auto",
686
+ stream: true,
687
+ temperature: options.temperature,
688
+ max_tokens: options.maxTokens,
689
+ }),
690
+ signal: createAbortSignalWithTimeout(this.timeout),
710
691
  });
711
- iteration++;
692
+ if (!response.ok) {
693
+ throw this.handleProviderError(new Error(`Ollama API error: ${response.status} ${response.statusText}`));
694
+ }
695
+ // Process response stream
696
+ const { content, toolCalls, finishReason } = await this.processOllamaResponse(response, controller);
697
+ // Add assistant message to history
698
+ const assistantMessage = {
699
+ role: "assistant",
700
+ content: content || "",
701
+ };
702
+ if (toolCalls && toolCalls.length > 0) {
703
+ assistantMessage.tool_calls = toolCalls;
704
+ }
705
+ conversationHistory.push(assistantMessage);
706
+ // Check finish reason
707
+ if (finishReason === "stop" || !finishReason) {
708
+ // Conversation complete
709
+ span.setAttribute(ATTR.GEN_AI_FINISH_REASON, finishReason || "stop");
710
+ controller.close();
711
+ break;
712
+ }
713
+ else if (finishReason === "tool_calls" &&
714
+ toolCalls &&
715
+ toolCalls.length > 0) {
716
+ // Execute tools
717
+ logger.debug(`[OllamaProvider] Executing ${toolCalls.length} tools`);
718
+ for (const tc of toolCalls) {
719
+ span.addEvent("tool_call", {
720
+ [ATTR.GEN_AI_TOOL_NAME]: tc.function.name,
721
+ });
722
+ }
723
+ const toolResults = await this.executeOllamaTools(toolCalls, options);
724
+ // Add tool results to conversation
725
+ const toolMessage = {
726
+ role: "tool",
727
+ content: JSON.stringify(toolResults),
728
+ };
729
+ conversationHistory.push(toolMessage);
730
+ iteration++;
731
+ }
732
+ else if (finishReason === "length") {
733
+ // Max tokens reached, continue conversation
734
+ logger.debug(`[OllamaProvider] Max tokens reached, continuing`);
735
+ conversationHistory.push({
736
+ role: "user",
737
+ content: "Please continue.",
738
+ });
739
+ iteration++;
740
+ }
741
+ else {
742
+ // Unknown finish reason, end conversation
743
+ logger.warn(`[OllamaProvider] Unknown finish reason: ${finishReason}`);
744
+ span.setAttribute(ATTR.GEN_AI_FINISH_REASON, finishReason);
745
+ controller.close();
746
+ break;
747
+ }
712
748
  }
713
- else {
714
- // Unknown finish reason, end conversation
715
- logger.warn(`[OllamaProvider] Unknown finish reason: ${finishReason}`);
716
- controller.close();
717
- break;
749
+ if (iteration >= maxIterations) {
750
+ controller.error(new Error(`Ollama conversation exceeded maximum iterations (${maxIterations})`));
718
751
  }
719
752
  }
720
- if (iteration >= maxIterations) {
721
- controller.error(new Error(`Ollama conversation exceeded maximum iterations (${maxIterations})`));
753
+ catch (error) {
754
+ controller.error(error);
722
755
  }
723
- }
724
- catch (error) {
725
- controller.error(error);
726
- }
727
- },
756
+ finally {
757
+ // Resolve analytics with final values now that the loop has completed.
758
+ resolveAnalytics(createAnalytics(this.providerName, this.modelName || FALLBACK_OLLAMA_MODEL, { usage: { input: 0, output: 0, total: 0 } }, Date.now() - startTime, {
759
+ requestId: `ollama-stream-${Date.now()}`,
760
+ streamingMode: true,
761
+ iterations: iteration,
762
+ note: "Token usage not available from Ollama streaming responses",
763
+ }));
764
+ }
765
+ },
766
+ });
767
+ // Defer analytics resolution until the stream's start callback finishes.
768
+ // This ensures responseTime and iteration reflect the actual completed values
769
+ // rather than values captured before the tool-loop executes.
770
+ let resolveAnalytics;
771
+ const analyticsPromise = new Promise((resolve) => {
772
+ resolveAnalytics = resolve;
773
+ });
774
+ return {
775
+ stream: this.convertToAsyncIterable(stream),
776
+ provider: this.providerName,
777
+ model: this.modelName || FALLBACK_OLLAMA_MODEL,
778
+ analytics: analyticsPromise,
779
+ metadata: {
780
+ startTime,
781
+ streamId: `ollama-${Date.now()}`,
782
+ },
783
+ };
728
784
  });
729
- // Create analytics promise
730
- const analyticsPromise = Promise.resolve(createAnalytics(this.providerName, this.modelName || FALLBACK_OLLAMA_MODEL, { usage: { input: 0, output: 0, total: 0 } }, Date.now() - startTime, {
731
- requestId: `ollama-stream-${Date.now()}`,
732
- streamingMode: true,
733
- iterations: iteration,
734
- note: "Token usage not available from Ollama streaming responses",
735
- }));
736
- return {
737
- stream: this.convertToAsyncIterable(stream),
738
- provider: this.providerName,
739
- model: this.modelName || FALLBACK_OLLAMA_MODEL,
740
- analytics: analyticsPromise,
741
- metadata: {
742
- startTime,
743
- streamId: `ollama-${Date.now()}`,
744
- },
745
- };
746
785
  }
747
786
  /**
748
787
  * Execute streaming without tools using the generate API
749
788
  * Fallback for non-tool scenarios or when chat API is unavailable
750
789
  */
751
790
  async executeStreamWithoutTools(options, _analysisSchema) {
752
- // Validate that PDFs are not provided
753
- if (options.input?.pdfFiles && options.input.pdfFiles.length > 0) {
754
- throw new Error("PDF inputs are not supported by OllamaProvider. " +
755
- "Please remove PDFs or use a supported provider (OpenAI, Anthropic, Google Vertex AI, etc.).");
756
- }
757
- // Check for multimodal input
758
- const hasMultimodalInput = !!(options.input?.images?.length ||
759
- options.input?.content?.length ||
760
- options.input?.files?.length ||
761
- options.input?.csvFiles?.length);
762
- const useOpenAIMode = isOpenAICompatibleMode();
763
- if (useOpenAIMode) {
764
- // OpenAI-compatible mode: Use /v1/chat/completions with messages
765
- logger.debug(`Ollama (OpenAI mode): Building messages for streaming`);
766
- const messages = [];
767
- if (options.systemPrompt) {
768
- messages.push({ role: "system", content: options.systemPrompt });
791
+ return withClientSpan({
792
+ name: "neurolink.provider.stream",
793
+ tracer: tracers.provider,
794
+ attributes: {
795
+ [ATTR.GEN_AI_SYSTEM]: "ollama",
796
+ [ATTR.GEN_AI_MODEL]: this.modelName || FALLBACK_OLLAMA_MODEL,
797
+ [ATTR.GEN_AI_OPERATION]: "stream",
798
+ [ATTR.NL_HAS_TOOLS]: false,
799
+ [ATTR.NL_STREAM_MODE]: true,
800
+ },
801
+ }, async () => {
802
+ // Validate that PDFs are not provided
803
+ if (options.input?.pdfFiles && options.input.pdfFiles.length > 0) {
804
+ throw this.handleProviderError(new Error("PDF inputs are not supported by OllamaProvider. " +
805
+ "Please remove PDFs or use a supported provider (OpenAI, Anthropic, Google Vertex AI, etc.)."));
769
806
  }
770
- if (hasMultimodalInput) {
771
- const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
772
- const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
773
- // Convert multimodal messages to text (OpenAI-compatible mode doesn't support images in /v1/chat/completions for Ollama)
774
- const content = multimodalMessages
775
- .map((msg) => (typeof msg.content === "string" ? msg.content : ""))
776
- .join("\n");
777
- messages.push({ role: "user", content });
807
+ // Check for multimodal input
808
+ const hasMultimodalInput = !!(options.input?.images?.length ||
809
+ options.input?.content?.length ||
810
+ options.input?.files?.length ||
811
+ options.input?.csvFiles?.length);
812
+ const useOpenAIMode = isOpenAICompatibleMode();
813
+ if (useOpenAIMode) {
814
+ // OpenAI-compatible mode: Use /v1/chat/completions with messages
815
+ logger.debug(`Ollama (OpenAI mode): Building messages for streaming`);
816
+ const messages = [];
817
+ if (options.systemPrompt) {
818
+ messages.push({ role: "system", content: options.systemPrompt });
819
+ }
820
+ if (hasMultimodalInput) {
821
+ const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
822
+ const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
823
+ // Convert multimodal messages to text (OpenAI-compatible mode doesn't support images in /v1/chat/completions for Ollama)
824
+ const content = multimodalMessages
825
+ .map((msg) => typeof msg.content === "string" ? msg.content : "")
826
+ .join("\n");
827
+ messages.push({ role: "user", content });
828
+ }
829
+ else {
830
+ messages.push({ role: "user", content: options.input.text });
831
+ }
832
+ const requestUrl = `${this.baseUrl}/v1/chat/completions`;
833
+ const requestBody = {
834
+ model: this.modelName || FALLBACK_OLLAMA_MODEL,
835
+ messages,
836
+ temperature: options.temperature,
837
+ max_tokens: options.maxTokens,
838
+ stream: true,
839
+ };
840
+ logger.debug(`[Ollama OpenAI Mode] About to fetch:`, {
841
+ url: requestUrl,
842
+ baseUrl: this.baseUrl,
843
+ modelName: this.modelName,
844
+ requestBody: JSON.stringify(requestBody),
845
+ });
846
+ const response = await proxyFetch(requestUrl, {
847
+ method: "POST",
848
+ headers: { "Content-Type": "application/json" },
849
+ body: JSON.stringify(requestBody),
850
+ signal: createAbortSignalWithTimeout(this.timeout),
851
+ });
852
+ logger.debug(`[Ollama OpenAI Mode] Response received:`, {
853
+ status: response.status,
854
+ statusText: response.statusText,
855
+ ok: response.ok,
856
+ });
857
+ if (!response.ok) {
858
+ throw this.handleProviderError(new Error(`Ollama API error: ${response.status} ${response.statusText}`));
859
+ }
860
+ // Transform to async generator for OpenAI-compatible format
861
+ const self = this;
862
+ const transformedStream = async function* () {
863
+ const generator = self.createOpenAIStream(response);
864
+ for await (const chunk of generator) {
865
+ yield chunk;
866
+ }
867
+ };
868
+ return {
869
+ stream: transformedStream(),
870
+ provider: self.providerName,
871
+ model: self.modelName,
872
+ };
778
873
  }
779
874
  else {
780
- messages.push({ role: "user", content: options.input.text });
781
- }
782
- const requestUrl = `${this.baseUrl}/v1/chat/completions`;
783
- const requestBody = {
784
- model: this.modelName || FALLBACK_OLLAMA_MODEL,
785
- messages,
786
- temperature: options.temperature,
787
- max_tokens: options.maxTokens,
788
- stream: true,
789
- };
790
- logger.debug(`[Ollama OpenAI Mode] About to fetch:`, {
791
- url: requestUrl,
792
- baseUrl: this.baseUrl,
793
- modelName: this.modelName,
794
- requestBody: JSON.stringify(requestBody),
795
- });
796
- const response = await proxyFetch(requestUrl, {
797
- method: "POST",
798
- headers: { "Content-Type": "application/json" },
799
- body: JSON.stringify(requestBody),
800
- signal: createAbortSignalWithTimeout(this.timeout),
801
- });
802
- logger.debug(`[Ollama OpenAI Mode] Response received:`, {
803
- status: response.status,
804
- statusText: response.statusText,
805
- ok: response.ok,
806
- });
807
- if (!response.ok) {
808
- throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
809
- }
810
- // Transform to async generator for OpenAI-compatible format
811
- const self = this;
812
- const transformedStream = async function* () {
813
- const generator = self.createOpenAIStream(response);
814
- for await (const chunk of generator) {
815
- yield chunk;
875
+ // Native Ollama mode: Use /api/generate
876
+ let prompt = options.input.text;
877
+ let images;
878
+ if (hasMultimodalInput) {
879
+ logger.debug(`Ollama (native mode): Detected multimodal input`, {
880
+ hasImages: !!options.input?.images?.length,
881
+ imageCount: options.input?.images?.length || 0,
882
+ });
883
+ const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
884
+ const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
885
+ // Extract text from messages for prompt
886
+ prompt = multimodalMessages
887
+ .map((msg) => typeof msg.content === "string" ? msg.content : "")
888
+ .join("\n");
889
+ // Extract images
890
+ images = this.extractImagesFromMessages(multimodalMessages);
816
891
  }
817
- };
818
- return {
819
- stream: transformedStream(),
820
- provider: self.providerName,
821
- model: self.modelName,
822
- };
823
- }
824
- else {
825
- // Native Ollama mode: Use /api/generate
826
- let prompt = options.input.text;
827
- let images;
828
- if (hasMultimodalInput) {
829
- logger.debug(`Ollama (native mode): Detected multimodal input`, {
830
- hasImages: !!options.input?.images?.length,
831
- imageCount: options.input?.images?.length || 0,
892
+ const requestBody = {
893
+ model: this.modelName || FALLBACK_OLLAMA_MODEL,
894
+ prompt,
895
+ system: options.systemPrompt,
896
+ stream: true,
897
+ options: {
898
+ temperature: options.temperature,
899
+ num_predict: options.maxTokens,
900
+ },
901
+ };
902
+ if (images && images.length > 0) {
903
+ requestBody.images = images;
904
+ }
905
+ const requestUrl = `${this.baseUrl}/api/generate`;
906
+ logger.debug(`[Ollama Native Mode] About to fetch:`, {
907
+ url: requestUrl,
908
+ baseUrl: this.baseUrl,
909
+ modelName: this.modelName,
910
+ requestBody: JSON.stringify(requestBody),
832
911
  });
833
- const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
834
- const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
835
- // Extract text from messages for prompt
836
- prompt = multimodalMessages
837
- .map((msg) => (typeof msg.content === "string" ? msg.content : ""))
838
- .join("\n");
839
- // Extract images
840
- images = this.extractImagesFromMessages(multimodalMessages);
841
- }
842
- const requestBody = {
843
- model: this.modelName || FALLBACK_OLLAMA_MODEL,
844
- prompt,
845
- system: options.systemPrompt,
846
- stream: true,
847
- options: {
848
- temperature: options.temperature,
849
- num_predict: options.maxTokens,
850
- },
851
- };
852
- if (images && images.length > 0) {
853
- requestBody.images = images;
854
- }
855
- const requestUrl = `${this.baseUrl}/api/generate`;
856
- logger.debug(`[Ollama Native Mode] About to fetch:`, {
857
- url: requestUrl,
858
- baseUrl: this.baseUrl,
859
- modelName: this.modelName,
860
- requestBody: JSON.stringify(requestBody),
861
- });
862
- const response = await proxyFetch(requestUrl, {
863
- method: "POST",
864
- headers: { "Content-Type": "application/json" },
865
- body: JSON.stringify(requestBody),
866
- signal: createAbortSignalWithTimeout(this.timeout),
867
- });
868
- logger.debug(`[Ollama Native Mode] Response received:`, {
869
- status: response.status,
870
- statusText: response.statusText,
871
- ok: response.ok,
872
- });
873
- if (!response.ok) {
874
- throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
875
- }
876
- // Transform to async generator to match other providers
877
- const self = this;
878
- const transformedStream = async function* () {
879
- const generator = self.createOllamaStream(response);
880
- for await (const chunk of generator) {
881
- yield chunk;
912
+ const response = await proxyFetch(requestUrl, {
913
+ method: "POST",
914
+ headers: { "Content-Type": "application/json" },
915
+ body: JSON.stringify(requestBody),
916
+ signal: createAbortSignalWithTimeout(this.timeout),
917
+ });
918
+ logger.debug(`[Ollama Native Mode] Response received:`, {
919
+ status: response.status,
920
+ statusText: response.statusText,
921
+ ok: response.ok,
922
+ });
923
+ if (!response.ok) {
924
+ throw this.handleProviderError(new Error(`Ollama API error: ${response.status} ${response.statusText}`));
882
925
  }
883
- };
884
- return {
885
- stream: transformedStream(),
886
- provider: this.providerName,
887
- model: this.modelName,
888
- };
889
- }
926
+ // Transform to async generator to match other providers
927
+ const self = this;
928
+ const transformedStream = async function* () {
929
+ const generator = self.createOllamaStream(response);
930
+ for await (const chunk of generator) {
931
+ yield chunk;
932
+ }
933
+ };
934
+ return {
935
+ stream: transformedStream(),
936
+ provider: this.providerName,
937
+ model: this.modelName,
938
+ };
939
+ }
940
+ });
890
941
  }
891
942
  /**
892
943
  * Convert AI SDK tools format to Ollama's function calling format