@juspay/neurolink 9.42.0 → 9.43.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 (116) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/auth/anthropicOAuth.js +12 -0
  3. package/dist/browser/neurolink.min.js +335 -334
  4. package/dist/cli/commands/mcp.d.ts +6 -0
  5. package/dist/cli/commands/mcp.js +200 -184
  6. package/dist/cli/commands/proxy.js +560 -518
  7. package/dist/core/baseProvider.d.ts +6 -1
  8. package/dist/core/baseProvider.js +219 -232
  9. package/dist/core/factory.d.ts +3 -0
  10. package/dist/core/factory.js +140 -190
  11. package/dist/core/modules/ToolsManager.d.ts +1 -0
  12. package/dist/core/modules/ToolsManager.js +40 -42
  13. package/dist/core/toolEvents.d.ts +3 -0
  14. package/dist/core/toolEvents.js +7 -0
  15. package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
  16. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  17. package/dist/evaluation/scorers/scorerRegistry.js +356 -284
  18. package/dist/lib/auth/anthropicOAuth.js +12 -0
  19. package/dist/lib/core/baseProvider.d.ts +6 -1
  20. package/dist/lib/core/baseProvider.js +219 -232
  21. package/dist/lib/core/factory.d.ts +3 -0
  22. package/dist/lib/core/factory.js +140 -190
  23. package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
  24. package/dist/lib/core/modules/ToolsManager.js +40 -42
  25. package/dist/lib/core/toolEvents.d.ts +3 -0
  26. package/dist/lib/core/toolEvents.js +8 -0
  27. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
  28. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  29. package/dist/lib/evaluation/scorers/scorerRegistry.js +356 -284
  30. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  31. package/dist/lib/mcp/toolRegistry.js +32 -31
  32. package/dist/lib/neurolink.d.ts +38 -0
  33. package/dist/lib/neurolink.js +1890 -1707
  34. package/dist/lib/providers/googleAiStudio.js +0 -5
  35. package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
  36. package/dist/lib/providers/googleNativeGemini3.js +39 -1
  37. package/dist/lib/providers/googleVertex.d.ts +10 -0
  38. package/dist/lib/providers/googleVertex.js +445 -445
  39. package/dist/lib/providers/litellm.d.ts +1 -0
  40. package/dist/lib/providers/litellm.js +73 -64
  41. package/dist/lib/providers/ollama.js +17 -4
  42. package/dist/lib/providers/openAI.d.ts +2 -0
  43. package/dist/lib/providers/openAI.js +139 -140
  44. package/dist/lib/proxy/claudeFormat.js +14 -5
  45. package/dist/lib/proxy/oauthFetch.js +298 -318
  46. package/dist/lib/proxy/proxyConfig.js +3 -1
  47. package/dist/lib/proxy/proxyFetch.js +250 -222
  48. package/dist/lib/proxy/proxyHealth.d.ts +17 -0
  49. package/dist/lib/proxy/proxyHealth.js +55 -0
  50. package/dist/lib/proxy/requestLogger.js +140 -48
  51. package/dist/lib/proxy/routingPolicy.d.ts +33 -0
  52. package/dist/lib/proxy/routingPolicy.js +255 -0
  53. package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
  54. package/dist/lib/proxy/snapshotPersistence.js +41 -0
  55. package/dist/lib/proxy/sseInterceptor.js +36 -11
  56. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
  57. package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
  58. package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
  59. package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
  60. package/dist/lib/tasks/store/redisTaskStore.js +42 -17
  61. package/dist/lib/tasks/taskManager.d.ts +2 -0
  62. package/dist/lib/tasks/taskManager.js +100 -5
  63. package/dist/lib/telemetry/telemetryService.js +9 -5
  64. package/dist/lib/types/cli.d.ts +4 -0
  65. package/dist/lib/types/proxyTypes.d.ts +211 -1
  66. package/dist/lib/types/tools.d.ts +18 -0
  67. package/dist/lib/utils/providerHealth.d.ts +1 -0
  68. package/dist/lib/utils/providerHealth.js +46 -31
  69. package/dist/lib/utils/providerUtils.js +11 -22
  70. package/dist/lib/utils/schemaConversion.d.ts +1 -0
  71. package/dist/lib/utils/schemaConversion.js +3 -0
  72. package/dist/mcp/toolRegistry.d.ts +2 -0
  73. package/dist/mcp/toolRegistry.js +32 -31
  74. package/dist/neurolink.d.ts +38 -0
  75. package/dist/neurolink.js +1890 -1707
  76. package/dist/providers/googleAiStudio.js +0 -5
  77. package/dist/providers/googleNativeGemini3.d.ts +4 -0
  78. package/dist/providers/googleNativeGemini3.js +39 -1
  79. package/dist/providers/googleVertex.d.ts +10 -0
  80. package/dist/providers/googleVertex.js +445 -445
  81. package/dist/providers/litellm.d.ts +1 -0
  82. package/dist/providers/litellm.js +73 -64
  83. package/dist/providers/ollama.js +17 -4
  84. package/dist/providers/openAI.d.ts +2 -0
  85. package/dist/providers/openAI.js +139 -140
  86. package/dist/proxy/claudeFormat.js +14 -5
  87. package/dist/proxy/oauthFetch.js +298 -318
  88. package/dist/proxy/proxyConfig.js +3 -1
  89. package/dist/proxy/proxyFetch.js +250 -222
  90. package/dist/proxy/proxyHealth.d.ts +17 -0
  91. package/dist/proxy/proxyHealth.js +54 -0
  92. package/dist/proxy/requestLogger.js +140 -48
  93. package/dist/proxy/routingPolicy.d.ts +33 -0
  94. package/dist/proxy/routingPolicy.js +254 -0
  95. package/dist/proxy/snapshotPersistence.d.ts +2 -0
  96. package/dist/proxy/snapshotPersistence.js +40 -0
  97. package/dist/proxy/sseInterceptor.js +36 -11
  98. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
  99. package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
  100. package/dist/services/server/ai/observability/instrumentation.js +194 -218
  101. package/dist/tasks/backends/bullmqBackend.js +24 -18
  102. package/dist/tasks/store/redisTaskStore.js +42 -17
  103. package/dist/tasks/taskManager.d.ts +2 -0
  104. package/dist/tasks/taskManager.js +100 -5
  105. package/dist/telemetry/telemetryService.js +9 -5
  106. package/dist/types/cli.d.ts +4 -0
  107. package/dist/types/proxyTypes.d.ts +211 -1
  108. package/dist/types/tools.d.ts +18 -0
  109. package/dist/utils/providerHealth.d.ts +1 -0
  110. package/dist/utils/providerHealth.js +46 -31
  111. package/dist/utils/providerUtils.js +12 -22
  112. package/dist/utils/schemaConversion.d.ts +1 -0
  113. package/dist/utils/schemaConversion.js +3 -0
  114. package/package.json +3 -2
  115. package/scripts/observability/check-proxy-telemetry.mjs +1 -1
  116. package/scripts/observability/manage-local-openobserve.sh +36 -5
@@ -29,6 +29,7 @@ export declare class LiteLLMProvider extends BaseProvider {
29
29
  * Note: This is only used when tools are disabled
30
30
  */
31
31
  protected executeStream(options: StreamOptions, analysisSchema?: ZodType | Schema<unknown>): Promise<StreamResult>;
32
+ private createLiteLLMTransformedStream;
32
33
  /**
33
34
  * Generate an embedding for a single text input
34
35
  * Uses the LiteLLM proxy with OpenAI-compatible embedding API
@@ -1,6 +1,6 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
2
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
3
- import { NoOutputGeneratedError, Output, streamText } from "ai";
3
+ import { NoOutputGeneratedError, Output, streamText, } from "ai";
4
4
  import { BaseProvider } from "../core/baseProvider.js";
5
5
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
6
6
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
@@ -10,7 +10,7 @@ import { isAbortError } from "../utils/errorHandling.js";
10
10
  import { logger } from "../utils/logger.js";
11
11
  import { calculateCost } from "../utils/pricing.js";
12
12
  import { getProviderModel } from "../utils/providerConfig.js";
13
- import { composeAbortSignals, createTimeoutController, TimeoutError } from "../utils/timeout.js";
13
+ import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
14
14
  import { resolveToolChoice } from "../utils/toolChoice.js";
15
15
  import { getModelId } from "./providerTypeUtils.js";
16
16
  const streamTracer = trace.getTracer("neurolink.provider.litellm");
@@ -86,15 +86,18 @@ export class LiteLLMProvider extends BaseProvider {
86
86
  // Check for timeout by error name and message as fallback
87
87
  const errorRecord = error;
88
88
  if (errorRecord?.name === "TimeoutError" ||
89
- (typeof errorRecord?.message === "string" && errorRecord.message.toLowerCase().includes("timeout"))) {
89
+ (typeof errorRecord?.message === "string" &&
90
+ errorRecord.message.toLowerCase().includes("timeout"))) {
90
91
  return new NetworkError(`Request timed out: ${errorRecord?.message || "Unknown timeout"}`, this.providerName);
91
92
  }
92
93
  if (typeof errorRecord?.message === "string") {
93
- if (errorRecord.message.includes("ECONNREFUSED") || errorRecord.message.includes("Failed to fetch")) {
94
+ if (errorRecord.message.includes("ECONNREFUSED") ||
95
+ errorRecord.message.includes("Failed to fetch")) {
94
96
  return new NetworkError("LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
95
97
  `${process.env.LITELLM_BASE_URL || "http://localhost:4000"}`, this.providerName);
96
98
  }
97
- if (errorRecord.message.includes("API_KEY_INVALID") || errorRecord.message.includes("Invalid API key")) {
99
+ if (errorRecord.message.includes("API_KEY_INVALID") ||
100
+ errorRecord.message.includes("Invalid API key")) {
98
101
  return new AuthenticationError("Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.", this.providerName);
99
102
  }
100
103
  if (errorRecord.message.toLowerCase().includes("rate limit")) {
@@ -131,7 +134,9 @@ export class LiteLLMProvider extends BaseProvider {
131
134
  const model = await this.getAISDKModelWithMiddleware(options); // This is where network connection happens!
132
135
  // Get tools - options.tools is pre-merged by BaseProvider.stream()
133
136
  const shouldUseTools = !options.disableTools && this.supportsTools();
134
- const tools = shouldUseTools ? options.tools || (await this.getAllTools()) : {};
137
+ const tools = shouldUseTools
138
+ ? options.tools || (await this.getAllTools())
139
+ : {};
135
140
  logger.debug(`LiteLLM: Tools for streaming`, {
136
141
  shouldUseTools,
137
142
  toolCount: Object.keys(tools).length,
@@ -188,7 +193,8 @@ export class LiteLLMProvider extends BaseProvider {
188
193
  toolName: toolCall.toolName,
189
194
  args: toolCall.args ??
190
195
  toolCall.input ??
191
- toolCall.parameters ??
196
+ toolCall
197
+ .parameters ??
192
198
  {},
193
199
  });
194
200
  }
@@ -197,7 +203,8 @@ export class LiteLLMProvider extends BaseProvider {
197
203
  collectedToolResults.push({
198
204
  toolName: toolResult.toolName,
199
205
  status: rawToolResult.error ? "failure" : "success",
200
- output: (rawToolResult.output ?? rawToolResult.result) ?? undefined,
206
+ output: (rawToolResult.output ??
207
+ rawToolResult.result) ?? undefined,
201
208
  error: rawToolResult.error,
202
209
  id: rawToolResult.toolCallId ?? toolResult.toolName,
203
210
  });
@@ -243,7 +250,9 @@ export class LiteLLMProvider extends BaseProvider {
243
250
  catch (streamError) {
244
251
  streamSpan.setStatus({
245
252
  code: SpanStatusCode.ERROR,
246
- message: streamError instanceof Error ? streamError.message : String(streamError),
253
+ message: streamError instanceof Error
254
+ ? streamError.message
255
+ : String(streamError),
247
256
  });
248
257
  streamSpan.end();
249
258
  throw streamError;
@@ -285,59 +294,11 @@ export class LiteLLMProvider extends BaseProvider {
285
294
  streamSpan.end();
286
295
  });
287
296
  timeoutController?.cleanup();
288
- // Transform stream to content object stream using fullStream (handles both text and tool calls)
289
- // Note: fullStream includes tool results, textStream only has text
290
- const transformedStream = (async function* () {
291
- try {
292
- // Try fullStream first (handles both text and tool calls), fallback to textStream
293
- const streamToUse = result.fullStream || result.textStream;
294
- for await (const chunk of streamToUse) {
295
- // Handle different chunk types from fullStream
296
- if (chunk && typeof chunk === "object") {
297
- // Check for error chunks first (critical error handling)
298
- if ("type" in chunk && chunk.type === "error") {
299
- const errorChunk = chunk;
300
- logger.error(`LiteLLM: Error chunk received:`, {
301
- errorType: errorChunk.type,
302
- errorDetails: errorChunk.error,
303
- });
304
- throw new Error(`LiteLLM streaming error: ${errorChunk.error?.message || "Unknown error"}`);
305
- }
306
- if ("textDelta" in chunk) {
307
- // Text delta from fullStream
308
- const textDelta = chunk.textDelta;
309
- if (textDelta) {
310
- yield { content: textDelta };
311
- }
312
- }
313
- else if ("type" in chunk && chunk.type === "tool-call" && "toolCallId" in chunk) {
314
- // Tool call event - log for debugging
315
- const toolCallId = String(chunk.toolCallId);
316
- const toolName = "toolName" in chunk ? String(chunk.toolName) : "unknown";
317
- logger.debug("LiteLLM: Tool call", {
318
- toolCallId,
319
- toolName,
320
- });
321
- }
322
- }
323
- else if (typeof chunk === "string") {
324
- // Direct string chunk from textStream fallback
325
- yield { content: chunk };
326
- }
327
- }
328
- }
329
- catch (streamError) {
330
- // AI SDK v6 throws NoOutputGeneratedError when the stream produced no output.
331
- if (NoOutputGeneratedError.isInstance(streamError)) {
332
- logger.warn("LiteLLM: Stream produced no output (NoOutputGeneratedError)");
333
- return;
334
- }
335
- throw streamError;
336
- }
337
- })();
297
+ const transformedStream = this.createLiteLLMTransformedStream(result);
338
298
  // Create analytics promise that resolves after stream completion
339
299
  const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
340
- requestId: options.requestId ?? `litellm-stream-${Date.now()}`,
300
+ requestId: options.requestId ??
301
+ `litellm-stream-${Date.now()}`,
341
302
  streamingMode: true,
342
303
  });
343
304
  return {
@@ -360,6 +321,47 @@ export class LiteLLMProvider extends BaseProvider {
360
321
  throw this.handleProviderError(error);
361
322
  }
362
323
  }
324
+ async *createLiteLLMTransformedStream(result) {
325
+ try {
326
+ const streamToUse = result.fullStream || result.textStream;
327
+ for await (const chunk of streamToUse) {
328
+ if (chunk && typeof chunk === "object") {
329
+ if ("type" in chunk && chunk.type === "error") {
330
+ const errorChunk = chunk;
331
+ logger.error(`LiteLLM: Error chunk received:`, {
332
+ errorType: errorChunk.type,
333
+ errorDetails: errorChunk.error,
334
+ });
335
+ throw this.formatProviderError(new Error(`LiteLLM streaming error: ${errorChunk.error?.message || "Unknown error"}`));
336
+ }
337
+ if ("textDelta" in chunk) {
338
+ const textDelta = chunk.textDelta;
339
+ if (textDelta) {
340
+ yield { content: textDelta };
341
+ }
342
+ }
343
+ else if ("type" in chunk &&
344
+ chunk.type === "tool-call" &&
345
+ "toolCallId" in chunk) {
346
+ logger.debug("LiteLLM: Tool call", {
347
+ toolCallId: String(chunk.toolCallId),
348
+ toolName: "toolName" in chunk ? String(chunk.toolName) : "unknown",
349
+ });
350
+ }
351
+ }
352
+ else if (typeof chunk === "string") {
353
+ yield { content: chunk };
354
+ }
355
+ }
356
+ }
357
+ catch (streamError) {
358
+ if (NoOutputGeneratedError.isInstance(streamError)) {
359
+ logger.warn("LiteLLM: Stream produced no output (NoOutputGeneratedError)");
360
+ return;
361
+ }
362
+ throw streamError;
363
+ }
364
+ }
363
365
  /**
364
366
  * Generate an embedding for a single text input
365
367
  * Uses the LiteLLM proxy with OpenAI-compatible embedding API
@@ -368,7 +370,9 @@ export class LiteLLMProvider extends BaseProvider {
368
370
  const { embed: aiEmbed } = await import("ai");
369
371
  const { createOpenAI } = await import("@ai-sdk/openai");
370
372
  const config = getLiteLLMConfig();
371
- const embeddingModelName = modelName || process.env.LITELLM_EMBEDDING_MODEL || "gemini-embedding-001";
373
+ const embeddingModelName = modelName ||
374
+ process.env.LITELLM_EMBEDDING_MODEL ||
375
+ "gemini-embedding-001";
372
376
  const customOpenAI = createOpenAI({
373
377
  baseURL: config.baseURL,
374
378
  apiKey: config.apiKey,
@@ -386,7 +390,9 @@ export class LiteLLMProvider extends BaseProvider {
386
390
  const { embedMany: aiEmbedMany } = await import("ai");
387
391
  const { createOpenAI } = await import("@ai-sdk/openai");
388
392
  const config = getLiteLLMConfig();
389
- const embeddingModelName = modelName || process.env.LITELLM_EMBEDDING_MODEL || "gemini-embedding-001";
393
+ const embeddingModelName = modelName ||
394
+ process.env.LITELLM_EMBEDDING_MODEL ||
395
+ "gemini-embedding-001";
390
396
  const customOpenAI = createOpenAI({
391
397
  baseURL: config.baseURL,
392
398
  apiKey: config.apiKey,
@@ -405,7 +411,8 @@ export class LiteLLMProvider extends BaseProvider {
405
411
  const now = Date.now();
406
412
  // Check if cached models are still valid
407
413
  if (LiteLLMProvider.modelsCache.length > 0 &&
408
- now - LiteLLMProvider.modelsCacheTime < LiteLLMProvider.MODELS_CACHE_DURATION) {
414
+ now - LiteLLMProvider.modelsCacheTime <
415
+ LiteLLMProvider.MODELS_CACHE_DURATION) {
409
416
  logger.debug(`[${functionTag}] Using cached models`, {
410
417
  cacheAge: Math.round((now - LiteLLMProvider.modelsCacheTime) / 1000),
411
418
  modelCount: LiteLLMProvider.modelsCache.length,
@@ -431,7 +438,9 @@ export class LiteLLMProvider extends BaseProvider {
431
438
  });
432
439
  }
433
440
  // Fallback to hardcoded list if API fetch fails
434
- const fallbackModels = process.env.LITELLM_FALLBACK_MODELS?.split(",").map((m) => m.trim()) || [
441
+ const fallbackModels = process.env.LITELLM_FALLBACK_MODELS?.split(",")
442
+ .map((m) => m.trim())
443
+ .filter((m) => m.length > 0) || [
435
444
  "openai/gpt-4o", // minimal safe baseline
436
445
  "anthropic/claude-3-haiku",
437
446
  "meta-llama/llama-3.1-8b-instruct",
@@ -40,6 +40,11 @@ const getOllamaTimeout = () => {
40
40
  // especially for larger models like aliafshar/gemma3-it-qat-tools:latest (12.2B parameters)
41
41
  return parseInt(process.env.OLLAMA_TIMEOUT || "240000", 10);
42
42
  };
43
+ function isOllamaHttpError(error) {
44
+ return (error instanceof ProviderError &&
45
+ typeof error.statusCode === "number" &&
46
+ typeof error.responseBody === "string");
47
+ }
43
48
  async function createOllamaHttpError(response) {
44
49
  let responseBody = "";
45
50
  try {
@@ -49,7 +54,11 @@ async function createOllamaHttpError(response) {
49
54
  // Ignore unreadable bodies
50
55
  }
51
56
  const suffix = responseBody ? ` - ${responseBody.slice(0, 500)}` : "";
52
- return new Error(`Ollama API error: ${response.status} ${response.statusText}${suffix}`);
57
+ const error = new ProviderError(`Ollama API error: ${response.status} ${response.statusText}${suffix}`, "ollama");
58
+ error.statusCode = response.status;
59
+ error.statusText = response.statusText;
60
+ error.responseBody = responseBody;
61
+ return error;
53
62
  }
54
63
  // Create proxy-aware fetch instance
55
64
  const proxyFetch = createProxyFetch();
@@ -1538,12 +1547,16 @@ export class OllamaProvider extends BaseProvider {
1538
1547
  return new InvalidModelError(`❌ Ollama Model Not Found\n\nModel '${this.modelName}' is not available locally.\n\n🔧 Install Model:\n1. Run: ollama pull ${this.modelName}\n2. Or try a different model:\n - ollama pull ${FALLBACK_OLLAMA_MODEL}\n - ollama pull mistral:latest\n - ollama pull codellama:latest\n\n🔧 List Available Models:\nollama list`, this.providerName);
1539
1548
  }
1540
1549
  const errMsg = error.message ?? "";
1541
- if (errMsg.includes("404") &&
1542
- (errMsg.toLowerCase().includes("model") ||
1550
+ const httpStatus = isOllamaHttpError(error) ? error.statusCode : undefined;
1551
+ const responseBody = isOllamaHttpError(error) ? error.responseBody : "";
1552
+ if (httpStatus === 404 &&
1553
+ (responseBody.toLowerCase().includes("model") ||
1554
+ responseBody.toLowerCase().includes("not found") ||
1555
+ errMsg.toLowerCase().includes("model") ||
1543
1556
  errMsg.toLowerCase().includes("not found"))) {
1544
1557
  return new InvalidModelError(`❌ Ollama Returned HTTP 404\n\nThis usually means the configured model '${this.modelName}' is not installed locally, although a bad base URL or incompatible API mode can also cause it.\n\n🔧 Check:\n1. Verify the model exists: 'ollama list'\n2. Pull it if missing: 'ollama pull ${this.modelName}'\n3. Verify the service is healthy: 'curl ${this.baseUrl}/api/version'\n4. If you use OpenAI-compatible mode, confirm the base URL serves /v1/chat/completions`, this.providerName);
1545
1558
  }
1546
- if (errMsg.includes("404")) {
1559
+ if (httpStatus === 404) {
1547
1560
  return new ProviderError(`❌ Ollama Endpoint Returned HTTP 404\n\nThe configured base URL (${this.baseUrl}) did not serve the expected Ollama endpoint for model '${this.modelName}'. This is usually a configuration or API-mode mismatch rather than a missing model.\n\n🔧 Check:\n1. Verify the base URL: ${this.baseUrl}\n2. For native Ollama mode, confirm /api/generate exists\n3. For OpenAI-compatible mode, confirm /v1/chat/completions exists\n4. If the model is missing, the response body should explicitly say so`, this.providerName);
1548
1561
  }
1549
1562
  return new ProviderError(`❌ Ollama Provider Error\n\n${error.message || "Unknown error occurred"}\n\n🔧 Troubleshooting:\n1. Check if Ollama service is running\n2. Verify model is installed: 'ollama list'\n3. Check network connectivity to ${this.baseUrl}\n4. Review Ollama logs for details`, this.providerName);
@@ -52,6 +52,8 @@ export declare class OpenAIProvider extends BaseProvider {
52
52
  * and the migration guide in the project repository.
53
53
  */
54
54
  protected executeStream(options: StreamOptions, _analysisSchema?: ValidationSchema): Promise<StreamResult>;
55
+ private createOpenAITransformedStream;
56
+ private extractOpenAIChunkContent;
55
57
  /**
56
58
  * Generate embeddings for text using OpenAI text-embedding models
57
59
  * @param text - The text to embed
@@ -279,6 +279,16 @@ export class OpenAIProvider extends BaseProvider {
279
279
  // Build message array from options with multimodal support
280
280
  // Using protected helper from BaseProvider to eliminate code duplication
281
281
  const messages = await this.buildMessagesForStream(options);
282
+ let resolvedToolChoice = resolveToolChoice(options, tools, shouldUseTools);
283
+ // Guard: if toolChoice names a specific tool that was filtered out, fall back to "auto"
284
+ if (resolvedToolChoice !== null &&
285
+ typeof resolvedToolChoice === "object" &&
286
+ "toolName" in resolvedToolChoice &&
287
+ typeof resolvedToolChoice.toolName === "string" &&
288
+ !tools[resolvedToolChoice.toolName]) {
289
+ logger.warn(`OpenAI: toolChoice references tool "${resolvedToolChoice.toolName}" which was removed during filtering; falling back to "auto"`);
290
+ resolvedToolChoice = "auto";
291
+ }
282
292
  // Debug the actual request being sent to OpenAI
283
293
  logger.debug(`OpenAI: streamText request parameters:`, {
284
294
  modelName: this.modelName,
@@ -286,7 +296,7 @@ export class OpenAIProvider extends BaseProvider {
286
296
  temperature: options.temperature,
287
297
  maxTokens: options.maxTokens,
288
298
  toolsCount: Object.keys(tools).length,
289
- toolChoice: shouldUseTools && Object.keys(tools).length > 0 ? "auto" : "none",
299
+ toolChoice: resolvedToolChoice,
290
300
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
291
301
  firstToolExample: Object.keys(tools).length > 0
292
302
  ? {
@@ -315,7 +325,7 @@ export class OpenAIProvider extends BaseProvider {
315
325
  maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
316
326
  tools,
317
327
  stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
318
- toolChoice: resolveToolChoice(options, tools, shouldUseTools),
328
+ toolChoice: resolvedToolChoice,
319
329
  abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
320
330
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
321
331
  onStepFinish: ({ toolCalls, toolResults }) => {
@@ -382,150 +392,14 @@ export class OpenAIProvider extends BaseProvider {
382
392
  hasToolResults: !!result.toolResults,
383
393
  resultType: typeof result,
384
394
  });
385
- // Transform string stream to content object stream using fullStream
386
- const transformedStream = async function* () {
387
- try {
388
- logger.debug(`OpenAI: Starting stream transformation`, {
389
- hasTextStream: !!result.textStream,
390
- hasFullStream: !!result.fullStream,
391
- resultKeys: Object.keys(result),
392
- toolsEnabled: shouldUseTools,
393
- toolsCount: Object.keys(tools).length,
394
- });
395
- let chunkCount = 0;
396
- let contentYielded = 0;
397
- // Try fullStream first (handles both text and tool calls), fallback to textStream
398
- const streamToUse = result.fullStream || result.textStream;
399
- if (!streamToUse) {
400
- logger.error("OpenAI: No stream available in result", {
401
- resultKeys: Object.keys(result),
402
- });
403
- return;
404
- }
405
- logger.debug(`OpenAI: Stream source selected:`, {
406
- usingFullStream: !!result.fullStream,
407
- usingTextStream: !!result.textStream && !result.fullStream,
408
- streamSourceType: result.fullStream ? "fullStream" : "textStream",
409
- });
410
- for await (const chunk of streamToUse) {
411
- chunkCount++;
412
- logger.debug(`OpenAI: Processing chunk ${chunkCount}:`, {
413
- chunkType: typeof chunk,
414
- chunkValue: typeof chunk === "string"
415
- ? chunk.substring(0, 50)
416
- : "not-string",
417
- chunkKeys: chunk && typeof chunk === "object"
418
- ? Object.keys(chunk)
419
- : "not-object",
420
- hasText: chunk && typeof chunk === "object" && "text" in chunk,
421
- hasTextDelta: chunk && typeof chunk === "object" && "textDelta" in chunk,
422
- hasType: chunk && typeof chunk === "object" && "type" in chunk,
423
- chunkTypeValue: chunk && typeof chunk === "object" && "type" in chunk
424
- ? chunk.type
425
- : "no-type",
426
- });
427
- let contentToYield = null;
428
- // Handle different chunk types from fullStream
429
- if (chunk && typeof chunk === "object") {
430
- // Log the full chunk structure for debugging (debug mode only)
431
- if (process.env.NEUROLINK_DEBUG === "true") {
432
- logger.debug(`OpenAI: Full chunk structure:`, {
433
- chunkKeys: Object.keys(chunk),
434
- fullChunk: JSON.stringify(chunk).substring(0, 500),
435
- });
436
- }
437
- if ("type" in chunk && chunk.type === "error") {
438
- // Handle error chunks when tools are enabled
439
- const errorChunk = chunk;
440
- logger.error(`OpenAI: Error chunk received:`, {
441
- errorType: errorChunk.type,
442
- errorDetails: errorChunk.error,
443
- fullChunk: JSON.stringify(chunk),
444
- });
445
- // Throw a more descriptive error for tool-related issues
446
- const errorMessage = errorChunk.error &&
447
- typeof errorChunk.error === "object" &&
448
- "message" in errorChunk.error
449
- ? String(errorChunk.error.message)
450
- : "OpenAI API error when tools are enabled";
451
- throw new Error(`OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`);
452
- }
453
- else if ("type" in chunk &&
454
- chunk.type === "text-delta" &&
455
- "textDelta" in chunk) {
456
- // Text delta from fullStream
457
- contentToYield = chunk.textDelta;
458
- logger.debug(`OpenAI: Found text-delta:`, {
459
- textDelta: contentToYield,
460
- });
461
- }
462
- else if ("text" in chunk) {
463
- // Direct text chunk
464
- contentToYield = chunk.text;
465
- logger.debug(`OpenAI: Found direct text:`, {
466
- text: contentToYield,
467
- });
468
- }
469
- else {
470
- // Log unhandled chunks in debug mode only
471
- if (process.env.NEUROLINK_DEBUG === "true") {
472
- logger.debug(`OpenAI: Unhandled object chunk:`, {
473
- chunkKeys: Object.keys(chunk),
474
- chunkType: chunk.type || "no-type",
475
- fullChunk: JSON.stringify(chunk).substring(0, 500),
476
- });
477
- }
478
- }
479
- }
480
- else if (typeof chunk === "string") {
481
- // Direct string chunk from textStream
482
- contentToYield = chunk;
483
- logger.debug(`OpenAI: Found string chunk:`, {
484
- content: contentToYield,
485
- });
486
- }
487
- else {
488
- logger.warn(`OpenAI: Unhandled chunk type:`, {
489
- type: typeof chunk,
490
- value: String(chunk).substring(0, 100),
491
- });
492
- }
493
- if (contentToYield) {
494
- contentYielded++;
495
- logger.debug(`OpenAI: Yielding content ${contentYielded}:`, {
496
- content: contentToYield.substring(0, 50),
497
- length: contentToYield.length,
498
- });
499
- yield { content: contentToYield };
500
- }
501
- }
502
- logger.debug(`OpenAI: Stream transformation completed`, {
503
- totalChunks: chunkCount,
504
- contentYielded,
505
- success: contentYielded > 0,
506
- });
507
- if (contentYielded === 0) {
508
- logger.warn(`OpenAI: No content was yielded from stream despite processing ${chunkCount} chunks`);
509
- }
510
- }
511
- catch (streamError) {
512
- // AI SDK v6 throws NoOutputGeneratedError when the stream produced no output.
513
- // Treat as an empty stream rather than crashing with an unhandled rejection.
514
- if (NoOutputGeneratedError.isInstance(streamError)) {
515
- logger.warn("OpenAI: Stream produced no output (NoOutputGeneratedError)");
516
- return;
517
- }
518
- logger.error(`OpenAI: Stream transformation error:`, streamError);
519
- throw streamError;
520
- }
521
- };
395
+ const transformedStream = this.createOpenAITransformedStream(result, shouldUseTools, tools);
522
396
  // Create analytics promise that resolves after stream completion
523
397
  const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
524
398
  requestId: `openai-stream-${Date.now()}`,
525
399
  streamingMode: true,
526
400
  });
527
401
  return {
528
- stream: transformedStream(),
402
+ stream: transformedStream,
529
403
  provider: this.providerName,
530
404
  model: this.modelName,
531
405
  analytics: analyticsPromise,
@@ -540,6 +414,131 @@ export class OpenAIProvider extends BaseProvider {
540
414
  throw this.handleProviderError(error);
541
415
  }
542
416
  }
417
+ async *createOpenAITransformedStream(result, shouldUseTools, tools) {
418
+ try {
419
+ logger.debug(`OpenAI: Starting stream transformation`, {
420
+ hasTextStream: !!result.textStream,
421
+ hasFullStream: !!result.fullStream,
422
+ resultKeys: Object.keys(result),
423
+ toolsEnabled: shouldUseTools,
424
+ toolsCount: Object.keys(tools).length,
425
+ });
426
+ let chunkCount = 0;
427
+ let contentYielded = 0;
428
+ const streamToUse = result.fullStream || result.textStream;
429
+ if (!streamToUse) {
430
+ logger.error("OpenAI: No stream available in result", {
431
+ resultKeys: Object.keys(result),
432
+ });
433
+ return;
434
+ }
435
+ logger.debug(`OpenAI: Stream source selected:`, {
436
+ usingFullStream: !!result.fullStream,
437
+ usingTextStream: !!result.textStream && !result.fullStream,
438
+ streamSourceType: result.fullStream ? "fullStream" : "textStream",
439
+ });
440
+ for await (const chunk of streamToUse) {
441
+ chunkCount++;
442
+ logger.debug(`OpenAI: Processing chunk ${chunkCount}:`, {
443
+ chunkType: typeof chunk,
444
+ chunkValue: typeof chunk === "string"
445
+ ? chunk.substring(0, 50)
446
+ : "not-string",
447
+ chunkKeys: chunk && typeof chunk === "object"
448
+ ? Object.keys(chunk)
449
+ : "not-object",
450
+ hasText: chunk && typeof chunk === "object" && "text" in chunk,
451
+ hasTextDelta: chunk && typeof chunk === "object" && "textDelta" in chunk,
452
+ hasType: chunk && typeof chunk === "object" && "type" in chunk,
453
+ chunkTypeValue: chunk && typeof chunk === "object" && "type" in chunk
454
+ ? chunk.type
455
+ : "no-type",
456
+ });
457
+ const contentToYield = this.extractOpenAIChunkContent(chunk);
458
+ if (contentToYield) {
459
+ contentYielded++;
460
+ logger.debug(`OpenAI: Yielding content ${contentYielded}:`, {
461
+ content: contentToYield.substring(0, 50),
462
+ length: contentToYield.length,
463
+ });
464
+ yield { content: contentToYield };
465
+ }
466
+ }
467
+ logger.debug(`OpenAI: Stream transformation completed`, {
468
+ totalChunks: chunkCount,
469
+ contentYielded,
470
+ success: contentYielded > 0,
471
+ });
472
+ if (contentYielded === 0) {
473
+ logger.warn(`OpenAI: No content was yielded from stream despite processing ${chunkCount} chunks`);
474
+ }
475
+ }
476
+ catch (streamError) {
477
+ if (NoOutputGeneratedError.isInstance(streamError)) {
478
+ logger.warn("OpenAI: Stream produced no output (NoOutputGeneratedError)");
479
+ return;
480
+ }
481
+ logger.error(`OpenAI: Stream transformation error:`, streamError);
482
+ throw streamError;
483
+ }
484
+ }
485
+ extractOpenAIChunkContent(chunk) {
486
+ if (chunk && typeof chunk === "object") {
487
+ if (process.env.NEUROLINK_DEBUG === "true") {
488
+ logger.debug(`OpenAI: Full chunk structure:`, {
489
+ chunkKeys: Object.keys(chunk),
490
+ fullChunk: JSON.stringify(chunk).substring(0, 500),
491
+ });
492
+ }
493
+ if ("type" in chunk && chunk.type === "error") {
494
+ const errorChunk = chunk;
495
+ logger.error(`OpenAI: Error chunk received:`, {
496
+ errorType: errorChunk.type,
497
+ errorDetails: errorChunk.error,
498
+ fullChunk: JSON.stringify(chunk),
499
+ });
500
+ const errorMessage = errorChunk.error &&
501
+ typeof errorChunk.error === "object" &&
502
+ "message" in errorChunk.error
503
+ ? String(errorChunk.error.message)
504
+ : "OpenAI API error when tools are enabled";
505
+ throw new Error(`OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`);
506
+ }
507
+ if ("type" in chunk &&
508
+ chunk.type === "text-delta" &&
509
+ "textDelta" in chunk) {
510
+ const textDelta = chunk.textDelta;
511
+ logger.debug(`OpenAI: Found text-delta:`, { textDelta });
512
+ return textDelta;
513
+ }
514
+ if ("text" in chunk) {
515
+ const text = chunk.text;
516
+ logger.debug(`OpenAI: Found direct text:`, { text });
517
+ return text;
518
+ }
519
+ if (process.env.NEUROLINK_DEBUG === "true") {
520
+ logger.debug(`OpenAI: Unhandled object chunk:`, {
521
+ chunkKeys: Object.keys(chunk),
522
+ chunkType: "type" in chunk
523
+ ? String(chunk.type)
524
+ : "no-type",
525
+ fullChunk: JSON.stringify(chunk).substring(0, 500),
526
+ });
527
+ }
528
+ return null;
529
+ }
530
+ if (typeof chunk === "string") {
531
+ logger.debug(`OpenAI: Found string chunk:`, {
532
+ content: chunk,
533
+ });
534
+ return chunk;
535
+ }
536
+ logger.warn(`OpenAI: Unhandled chunk type:`, {
537
+ type: typeof chunk,
538
+ value: String(chunk).substring(0, 100),
539
+ });
540
+ return null;
541
+ }
543
542
  /**
544
543
  * Generate embeddings for text using OpenAI text-embedding models
545
544
  * @param text - The text to embed