@juspay/neurolink 9.6.0 → 9.7.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 (100) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/adapters/video/vertexVideoHandler.js +3 -3
  3. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  4. package/dist/cli/loop/optionsSchema.js +4 -0
  5. package/dist/core/analytics.js +11 -4
  6. package/dist/core/baseProvider.d.ts +6 -0
  7. package/dist/core/baseProvider.js +83 -14
  8. package/dist/core/conversationMemoryManager.d.ts +13 -0
  9. package/dist/core/conversationMemoryManager.js +28 -0
  10. package/dist/core/dynamicModels.js +3 -2
  11. package/dist/core/modules/GenerationHandler.js +2 -0
  12. package/dist/core/redisConversationMemoryManager.d.ts +11 -0
  13. package/dist/core/redisConversationMemoryManager.js +26 -9
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +5 -0
  16. package/dist/lib/adapters/video/vertexVideoHandler.js +3 -3
  17. package/dist/lib/core/analytics.js +11 -4
  18. package/dist/lib/core/baseProvider.d.ts +6 -0
  19. package/dist/lib/core/baseProvider.js +83 -14
  20. package/dist/lib/core/conversationMemoryManager.d.ts +13 -0
  21. package/dist/lib/core/conversationMemoryManager.js +28 -0
  22. package/dist/lib/core/dynamicModels.js +3 -2
  23. package/dist/lib/core/modules/GenerationHandler.js +2 -0
  24. package/dist/lib/core/redisConversationMemoryManager.d.ts +11 -0
  25. package/dist/lib/core/redisConversationMemoryManager.js +26 -9
  26. package/dist/lib/index.d.ts +4 -0
  27. package/dist/lib/index.js +5 -0
  28. package/dist/lib/mcp/httpRetryHandler.js +6 -2
  29. package/dist/lib/neurolink.d.ts +5 -0
  30. package/dist/lib/neurolink.js +160 -10
  31. package/dist/lib/processors/base/BaseFileProcessor.js +2 -1
  32. package/dist/lib/processors/errors/errorHelpers.js +12 -4
  33. package/dist/lib/providers/amazonBedrock.js +2 -1
  34. package/dist/lib/providers/anthropic.js +2 -2
  35. package/dist/lib/providers/anthropicBaseProvider.js +10 -4
  36. package/dist/lib/providers/azureOpenai.js +14 -25
  37. package/dist/lib/providers/googleAiStudio.d.ts +0 -34
  38. package/dist/lib/providers/googleAiStudio.js +124 -315
  39. package/dist/lib/providers/googleNativeGemini3.d.ts +119 -0
  40. package/dist/lib/providers/googleNativeGemini3.js +264 -0
  41. package/dist/lib/providers/googleVertex.d.ts +0 -40
  42. package/dist/lib/providers/googleVertex.js +150 -317
  43. package/dist/lib/providers/huggingFace.js +20 -5
  44. package/dist/lib/providers/litellm.js +6 -4
  45. package/dist/lib/providers/mistral.js +3 -2
  46. package/dist/lib/providers/openAI.js +2 -2
  47. package/dist/lib/providers/openRouter.js +8 -7
  48. package/dist/lib/providers/openaiCompatible.js +10 -4
  49. package/dist/lib/rag/resilience/RetryHandler.js +6 -2
  50. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +24 -2
  51. package/dist/lib/services/server/ai/observability/instrumentation.js +12 -1
  52. package/dist/lib/types/generateTypes.d.ts +28 -0
  53. package/dist/lib/types/ragTypes.d.ts +9 -1
  54. package/dist/lib/types/streamTypes.d.ts +13 -0
  55. package/dist/lib/utils/conversationMemory.js +15 -0
  56. package/dist/lib/utils/errorHandling.d.ts +5 -0
  57. package/dist/lib/utils/errorHandling.js +19 -0
  58. package/dist/lib/utils/pricing.d.ts +12 -0
  59. package/dist/lib/utils/pricing.js +134 -0
  60. package/dist/lib/utils/redis.d.ts +17 -0
  61. package/dist/lib/utils/redis.js +105 -0
  62. package/dist/lib/utils/timeout.d.ts +10 -0
  63. package/dist/lib/utils/timeout.js +15 -0
  64. package/dist/mcp/httpRetryHandler.js +6 -2
  65. package/dist/neurolink.d.ts +5 -0
  66. package/dist/neurolink.js +160 -10
  67. package/dist/processors/base/BaseFileProcessor.js +2 -1
  68. package/dist/processors/errors/errorHelpers.js +12 -4
  69. package/dist/providers/amazonBedrock.js +2 -1
  70. package/dist/providers/anthropic.js +2 -2
  71. package/dist/providers/anthropicBaseProvider.js +10 -4
  72. package/dist/providers/azureOpenai.js +14 -25
  73. package/dist/providers/googleAiStudio.d.ts +0 -34
  74. package/dist/providers/googleAiStudio.js +124 -315
  75. package/dist/providers/googleNativeGemini3.d.ts +119 -0
  76. package/dist/providers/googleNativeGemini3.js +263 -0
  77. package/dist/providers/googleVertex.d.ts +0 -40
  78. package/dist/providers/googleVertex.js +150 -317
  79. package/dist/providers/huggingFace.js +20 -5
  80. package/dist/providers/litellm.js +6 -4
  81. package/dist/providers/mistral.js +3 -2
  82. package/dist/providers/openAI.js +2 -2
  83. package/dist/providers/openRouter.js +8 -7
  84. package/dist/providers/openaiCompatible.js +10 -4
  85. package/dist/rag/resilience/RetryHandler.js +6 -2
  86. package/dist/services/server/ai/observability/instrumentation.d.ts +24 -2
  87. package/dist/services/server/ai/observability/instrumentation.js +12 -1
  88. package/dist/types/generateTypes.d.ts +28 -0
  89. package/dist/types/ragTypes.d.ts +9 -1
  90. package/dist/types/streamTypes.d.ts +13 -0
  91. package/dist/utils/conversationMemory.js +15 -0
  92. package/dist/utils/errorHandling.d.ts +5 -0
  93. package/dist/utils/errorHandling.js +19 -0
  94. package/dist/utils/pricing.d.ts +12 -0
  95. package/dist/utils/pricing.js +133 -0
  96. package/dist/utils/redis.d.ts +17 -0
  97. package/dist/utils/redis.js +105 -0
  98. package/dist/utils/timeout.d.ts +10 -0
  99. package/dist/utils/timeout.js +15 -0
  100. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
6
6
  import { createProxyFetch } from "../proxy/proxyFetch.js";
7
7
  import { logger } from "../utils/logger.js";
8
8
  import { createMistralConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
9
- import { createTimeoutController, TimeoutError } from "../utils/timeout.js";
9
+ import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
10
10
  // Configuration helpers - now using consolidated utility
11
11
  const getMistralApiKey = () => {
12
12
  return validateApiKey(createMistralConfig());
@@ -63,7 +63,8 @@ export class MistralProvider extends BaseProvider {
63
63
  tools,
64
64
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
65
65
  toolChoice: shouldUseTools ? "auto" : "none",
66
- abortSignal: timeoutController?.controller.signal,
66
+ abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
67
+ experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
67
68
  onStepFinish: ({ toolCalls, toolResults }) => {
68
69
  this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
69
70
  logger.warn("[MistralProvider] Failed to store tool executions", {
@@ -9,7 +9,7 @@ import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, Ra
9
9
  import { logger } from "../utils/logger.js";
10
10
  import { createOpenAIConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
11
11
  import { isZodSchema } from "../utils/schemaConversion.js";
12
- import { createTimeoutController, TimeoutError } from "../utils/timeout.js";
12
+ import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
13
13
  // Configuration helpers - now using consolidated utility
14
14
  const getOpenAIApiKey = () => {
15
15
  return validateApiKey(createOpenAIConfig());
@@ -282,7 +282,7 @@ export class OpenAIProvider extends BaseProvider {
282
282
  tools,
283
283
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
284
284
  toolChoice: shouldUseTools && Object.keys(tools).length > 0 ? "auto" : "none",
285
- abortSignal: timeoutController?.controller.signal,
285
+ abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
286
286
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
287
287
  onStepFinish: ({ toolCalls, toolResults }) => {
288
288
  logger.info("Tool execution completed", { toolResults, toolCalls });
@@ -1,13 +1,14 @@
1
1
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
2
- import { Output, streamText } from "ai";
2
+ import { Output, streamText, } from "ai";
3
3
  import { AIProviderName } from "../constants/enums.js";
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";
7
7
  import { createProxyFetch } from "../proxy/proxyFetch.js";
8
+ import { isAbortError } from "../utils/errorHandling.js";
8
9
  import { logger } from "../utils/logger.js";
9
10
  import { getProviderModel } from "../utils/providerConfig.js";
10
- import { createTimeoutController, TimeoutError } from "../utils/timeout.js";
11
+ import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
11
12
  // Constants
12
13
  const MODELS_DISCOVERY_TIMEOUT_MS = 5000; // 5 seconds for model discovery
13
14
  // Configuration helpers
@@ -206,8 +207,7 @@ export class OpenRouterProvider extends BaseProvider {
206
207
  // BaseProvider.stream() pre-merges base tools + external tools into options.tools
207
208
  const shouldUseTools = !options.disableTools && this.supportsTools();
208
209
  const tools = shouldUseTools
209
- ? options.tools ||
210
- (await this.getAllTools())
210
+ ? options.tools || (await this.getAllTools())
211
211
  : {};
212
212
  logger.debug(`OpenRouter: Tools for streaming`, {
213
213
  shouldUseTools,
@@ -229,7 +229,8 @@ export class OpenRouterProvider extends BaseProvider {
229
229
  toolChoice: "auto",
230
230
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
231
231
  }),
232
- abortSignal: timeoutController?.controller.signal,
232
+ abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
233
+ experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
233
234
  onError: (event) => {
234
235
  const error = event.error;
235
236
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -276,7 +277,7 @@ export class OpenRouterProvider extends BaseProvider {
276
277
  }
277
278
  }
278
279
  const result = await streamText(streamOptions);
279
- timeoutController?.cleanup();
280
+ result.text.finally(() => timeoutController?.cleanup());
280
281
  // Transform stream to content object stream using fullStream (handles both text and tool calls)
281
282
  const transformedStream = (async function* () {
282
283
  // Try fullStream first (handles both text and tool calls), fallback to textStream
@@ -439,7 +440,7 @@ export class OpenRouterProvider extends BaseProvider {
439
440
  }
440
441
  catch (error) {
441
442
  clearTimeout(timeoutId);
442
- if (error instanceof Error && error.name === "AbortError") {
443
+ if (isAbortError(error)) {
443
444
  throw new Error(`Request timed out after ${MODELS_DISCOVERY_TIMEOUT_MS / 1000} seconds`);
444
445
  }
445
446
  throw error;
@@ -3,7 +3,7 @@ import { streamText } from "ai";
3
3
  import { AIProviderName } from "../constants/enums.js";
4
4
  import { BaseProvider } from "../core/baseProvider.js";
5
5
  import { logger } from "../utils/logger.js";
6
- import { createTimeoutController, TimeoutError } from "../utils/timeout.js";
6
+ import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
7
7
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
8
8
  import { createProxyFetch } from "../proxy/proxyFetch.js";
9
9
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
@@ -159,6 +159,11 @@ export class OpenAICompatibleProvider extends BaseProvider {
159
159
  const timeout = this.getTimeout(options);
160
160
  const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
161
161
  try {
162
+ // Get tools - options.tools is pre-merged by BaseProvider.stream()
163
+ const shouldUseTools = !options.disableTools && this.supportsTools();
164
+ const tools = shouldUseTools
165
+ ? options.tools || (await this.getAllTools())
166
+ : {};
162
167
  // Build message array from options with multimodal support
163
168
  // Using protected helper from BaseProvider to eliminate code duplication
164
169
  const messages = await this.buildMessagesForStream(options);
@@ -173,9 +178,10 @@ export class OpenAICompatibleProvider extends BaseProvider {
173
178
  ? { temperature: options.temperature }
174
179
  : {}),
175
180
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
176
- tools: options.tools,
177
- toolChoice: "auto",
178
- abortSignal: timeoutController?.controller.signal,
181
+ tools,
182
+ toolChoice: shouldUseTools ? "auto" : "none",
183
+ abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
184
+ experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
179
185
  onStepFinish: ({ toolCalls, toolResults }) => {
180
186
  this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
181
187
  logger.warn("[OpenAiCompatibleProvider] Failed to store tool executions", {
@@ -6,6 +6,7 @@
6
6
  * vector queries, and LLM-based extraction.
7
7
  */
8
8
  import { withRetry, } from "../../core/infrastructure/index.js";
9
+ import { isAbortError } from "../../utils/errorHandling.js";
9
10
  import { logger } from "../../utils/logger.js";
10
11
  import { EmbeddingError, isRetryableRAGError, MetadataExtractionError, RAGError, RAGErrorCodes, VectorQueryError, } from "../errors/RAGError.js";
11
12
  /**
@@ -50,6 +51,10 @@ function sleep(ms) {
50
51
  * Check if an error is retryable based on configuration
51
52
  */
52
53
  export function isRetryable(error, config = DEFAULT_RAG_RETRY_CONFIG) {
54
+ // Never retry abort errors - the operation was intentionally cancelled
55
+ if (isAbortError(error)) {
56
+ return false;
57
+ }
53
58
  // Use custom shouldRetry if provided
54
59
  if (config.shouldRetry) {
55
60
  return config.shouldRetry(error);
@@ -70,8 +75,7 @@ export function isRetryable(error, config = DEFAULT_RAG_RETRY_CONFIG) {
70
75
  // Timeout errors
71
76
  if (errorObj.name === "TimeoutError" ||
72
77
  errorObj.code === "TIMEOUT" ||
73
- errorObj.code === "ETIMEDOUT" ||
74
- errorObj.name === "AbortError") {
78
+ errorObj.code === "ETIMEDOUT") {
75
79
  return true;
76
80
  }
77
81
  // Network errors
@@ -15,7 +15,7 @@ import type { LangfuseConfig } from "../../../../types/observability.js";
15
15
  * Extended context for Langfuse spans
16
16
  * Supports all Langfuse trace attributes for rich observability
17
17
  */
18
- type LangfuseContext = {
18
+ export type LangfuseContext = {
19
19
  userId?: string | null;
20
20
  sessionId?: string | null;
21
21
  /** Conversation/thread identifier for grouping related traces */
@@ -51,6 +51,27 @@ type LangfuseContext = {
51
51
  * @default undefined (uses global setting, which defaults to true)
52
52
  */
53
53
  autoDetectOperationName?: boolean;
54
+ /**
55
+ * Custom attributes to set on all spans within this context.
56
+ *
57
+ * These attributes are propagated to every span created within the
58
+ * AsyncLocalStorage context, enabling application-level context
59
+ * (e.g., Slack channel name, feature flag, tenant ID) to appear
60
+ * on all SDK-internal spans.
61
+ *
62
+ * @example
63
+ * await setLangfuseContext({
64
+ * userId: "user@email.com",
65
+ * customAttributes: {
66
+ * "app.slack.channel": "engineering",
67
+ * "app.tenant.id": "tenant-123",
68
+ * "app.feature.flag": true,
69
+ * }
70
+ * }, async () => {
71
+ * // All spans created here will have these attributes
72
+ * });
73
+ */
74
+ customAttributes?: Record<string, string | number | boolean>;
54
75
  };
55
76
  /**
56
77
  * Initialize OpenTelemetry with Langfuse span processor
@@ -140,6 +161,8 @@ export declare function setLangfuseContext<T = void>(context: {
140
161
  operationName?: string | null;
141
162
  /** Override global autoDetectOperationName for this context */
142
163
  autoDetectOperationName?: boolean;
164
+ /** Custom attributes to set on all spans within this context */
165
+ customAttributes?: Record<string, string | number | boolean>;
143
166
  }, callback?: () => T | Promise<T>): Promise<T | void>;
144
167
  /**
145
168
  * Get the current Langfuse context from AsyncLocalStorage
@@ -194,4 +217,3 @@ export declare function getSpanProcessors(): SpanProcessor[];
194
217
  * @returns true if operating in external TracerProvider mode
195
218
  */
196
219
  export declare function isUsingExternalTracerProvider(): boolean;
197
- export {};
@@ -119,7 +119,14 @@ class ContextEnricher {
119
119
  // 2. Formatted name with userId + operationName
120
120
  // 3. userId only (legacy fallback)
121
121
  const traceName = this.buildTraceName(context?.traceName, userId, operationName);
122
- // Set user and session attributes
122
+ // Apply custom attributes FIRST so internal attributes always take precedence
123
+ // and cannot be accidentally overwritten by user-provided values
124
+ if (context?.customAttributes) {
125
+ for (const [key, value] of Object.entries(context.customAttributes)) {
126
+ span.setAttribute(key, value);
127
+ }
128
+ }
129
+ // Set user and session attributes (internal - always override custom)
123
130
  if (userId && userId !== "guest") {
124
131
  span.setAttribute("user.id", userId);
125
132
  }
@@ -657,6 +664,10 @@ export async function setLangfuseContext(context, callback) {
657
664
  autoDetectOperationName: context.autoDetectOperationName !== undefined
658
665
  ? context.autoDetectOperationName
659
666
  : currentContext.autoDetectOperationName,
667
+ // Custom attributes support
668
+ customAttributes: context.customAttributes !== undefined
669
+ ? context.customAttributes
670
+ : currentContext.customAttributes,
660
671
  };
661
672
  if (callback) {
662
673
  return await contextStorage.run(newContext, callback);
@@ -217,6 +217,8 @@ export type GenerateOptions = {
217
217
  schema?: ValidationSchema;
218
218
  tools?: Record<string, Tool>;
219
219
  timeout?: number | string;
220
+ /** AbortSignal for external cancellation of the AI call */
221
+ abortSignal?: AbortSignal;
220
222
  /**
221
223
  * Disable tool execution (including built-in tools)
222
224
  *
@@ -235,6 +237,17 @@ export type GenerateOptions = {
235
237
  * ```
236
238
  */
237
239
  disableTools?: boolean;
240
+ /** Include only these tools by name (whitelist). If set, only matching tools are available. */
241
+ toolFilter?: string[];
242
+ /** Exclude these tools by name (blacklist). Applied after toolFilter. */
243
+ excludeTools?: string[];
244
+ /**
245
+ * Skip injecting tool schemas into the system prompt.
246
+ * When true, tools are ONLY passed natively via the provider's `tools` parameter,
247
+ * avoiding duplicate tool definitions (~30K tokens savings per call).
248
+ * Default: false (backward compatible — tool schemas are injected into system prompt).
249
+ */
250
+ skipToolPromptInjection?: boolean;
238
251
  enableEvaluation?: boolean;
239
252
  enableAnalytics?: boolean;
240
253
  context?: StandardRecord;
@@ -389,6 +402,7 @@ export type GenerateResult = {
389
402
  } | null;
390
403
  provider?: string;
391
404
  model?: string;
405
+ finishReason?: string;
392
406
  usage?: TokenUsage;
393
407
  responseTime?: number;
394
408
  toolCalls?: Array<{
@@ -546,8 +560,14 @@ export type TextGenerationOptions = {
546
560
  };
547
561
  tools?: Record<string, Tool>;
548
562
  timeout?: number | string;
563
+ /** AbortSignal for external cancellation of the AI call */
564
+ abortSignal?: AbortSignal;
549
565
  disableTools?: boolean;
550
566
  maxSteps?: number;
567
+ /** Include only these tools by name (whitelist). If set, only matching tools are available. */
568
+ toolFilter?: string[];
569
+ /** Exclude these tools by name (blacklist). Applied after toolFilter. */
570
+ excludeTools?: string[];
551
571
  /**
552
572
  * Text-to-Speech (TTS) configuration
553
573
  *
@@ -609,6 +629,13 @@ export type TextGenerationOptions = {
609
629
  * @internal Set by NeuroLink SDK — not typically used directly by consumers.
610
630
  */
611
631
  fileRegistry?: unknown;
632
+ /**
633
+ * Skip injecting tool schemas into the system prompt.
634
+ * When true, tools are ONLY passed natively via the provider's `tools` parameter,
635
+ * avoiding duplicate tool definitions (~30K tokens savings per call).
636
+ * Default: false (backward compatible — tool schemas are injected into system prompt).
637
+ */
638
+ skipToolPromptInjection?: boolean;
612
639
  /**
613
640
  * ## Extended Thinking Options
614
641
  *
@@ -702,6 +729,7 @@ export type TextGenerationOptions = {
702
729
  */
703
730
  export type TextGenerationResult = {
704
731
  content: string;
732
+ finishReason?: string;
705
733
  provider?: string;
706
734
  model?: string;
707
735
  usage?: TokenUsage;
@@ -133,7 +133,15 @@ export type RAGRetryConfig = {
133
133
  backoffMultiplier: number;
134
134
  /** Whether to add jitter (default: true) */
135
135
  jitter: boolean;
136
- /** Custom function to determine if error is retryable */
136
+ /**
137
+ * Custom function to determine if error is retryable.
138
+ *
139
+ * Note: In `isRetryable()`, this callback is invoked *before* the built-in
140
+ * abort-error check. If you provide a custom `shouldRetry`, it should
141
+ * explicitly handle abort errors (e.g. return `false` for them) when
142
+ * cancellation correctness is required. Otherwise an aborted operation
143
+ * could be retried instead of propagating immediately.
144
+ */
137
145
  shouldRetry?: (error: Error) => boolean;
138
146
  /** Retryable error codes */
139
147
  retryableErrorCodes?: string[];
@@ -321,8 +321,21 @@ export type StreamOptions = {
321
321
  schema?: ValidationSchema;
322
322
  tools?: Record<string, Tool>;
323
323
  timeout?: number | string;
324
+ /** AbortSignal for external cancellation of the AI call */
325
+ abortSignal?: AbortSignal;
324
326
  disableTools?: boolean;
325
327
  maxSteps?: number;
328
+ /** Include only these tools by name (whitelist). If set, only matching tools are available. */
329
+ toolFilter?: string[];
330
+ /** Exclude these tools by name (blacklist). Applied after toolFilter. */
331
+ excludeTools?: string[];
332
+ /**
333
+ * Skip injecting tool schemas into the system prompt.
334
+ * When true, tools are ONLY passed natively via the provider's `tools` parameter,
335
+ * avoiding duplicate tool definitions (~30K tokens savings per call).
336
+ * Default: false (backward compatible — tool schemas are injected into system prompt).
337
+ */
338
+ skipToolPromptInjection?: boolean;
326
339
  enableEvaluation?: boolean;
327
340
  enableAnalytics?: boolean;
328
341
  context?: UnknownRecord;
@@ -104,6 +104,21 @@ export async function storeConversationTurn(conversationMemory, originalOptions,
104
104
  }
105
105
  const userMessage = originalOptions.originalPrompt || originalOptions.prompt || "";
106
106
  const aiResponse = result.content ?? "";
107
+ // Guard: skip storing conversation turn if AI response is empty AND no tools were used.
108
+ // Empty assistant messages cause "text content blocks must be non-empty" errors
109
+ // when loaded as conversation history on the next interaction.
110
+ // However, tool-only turns (empty text but tools were invoked) must still be stored
111
+ // to preserve tool-calling conversation history.
112
+ const hasToolActivity = (result.toolsUsed && result.toolsUsed.length > 0) ||
113
+ (result.toolExecutions && result.toolExecutions.length > 0);
114
+ if (!aiResponse.trim() && !hasToolActivity) {
115
+ logger.warn("[conversationMemoryUtils] Skipping conversation turn storage — AI response is empty and no tool activity", {
116
+ sessionId,
117
+ userId,
118
+ userMessageLength: userMessage.length,
119
+ });
120
+ return;
121
+ }
107
122
  let providerDetails;
108
123
  if (result.provider && result.model) {
109
124
  providerDetails = {
@@ -244,6 +244,11 @@ export declare class CircuitBreaker {
244
244
  getState(): "closed" | "open" | "half-open";
245
245
  getFailureCount(): number;
246
246
  }
247
+ /**
248
+ * Detect AbortError from any source (DOMException, plain Error, or message-based).
249
+ * Used to short-circuit retry/fallback loops when an abort signal fires.
250
+ */
251
+ export declare function isAbortError(error: unknown): boolean;
247
252
  /**
248
253
  * Error handler that decides whether to retry based on error type
249
254
  */
@@ -820,6 +820,25 @@ export class CircuitBreaker {
820
820
  return this.failures;
821
821
  }
822
822
  }
823
+ /**
824
+ * Detect AbortError from any source (DOMException, plain Error, or message-based).
825
+ * Used to short-circuit retry/fallback loops when an abort signal fires.
826
+ */
827
+ export function isAbortError(error) {
828
+ if (error instanceof DOMException && error.name === "AbortError") {
829
+ return true;
830
+ }
831
+ if (error instanceof Error && error.name === "AbortError") {
832
+ return true;
833
+ }
834
+ if (error instanceof Error &&
835
+ (error.message === "This operation was aborted" ||
836
+ error.message === "The operation was aborted" ||
837
+ error.message?.includes("The user aborted a request"))) {
838
+ return true;
839
+ }
840
+ return false;
841
+ }
823
842
  /**
824
843
  * Error handler that decides whether to retry based on error type
825
844
  */
@@ -0,0 +1,12 @@
1
+ import type { TokenUsage } from "../types/analytics.js";
2
+ /**
3
+ * Calculate the dollar cost of a generate/stream call based on token usage.
4
+ * Returns 0 if the provider/model combination is not in the pricing table.
5
+ */
6
+ export declare function calculateCost(provider: string, model: string, usage: TokenUsage): number;
7
+ /**
8
+ * Check if pricing is available for a provider/model combination.
9
+ * Checks the rate table directly instead of computing a cost,
10
+ * so even very cheap models (e.g. gemini-1.5-flash) are detected correctly.
11
+ */
12
+ export declare function hasPricing(provider: string, model: string): boolean;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Per-token pricing data (USD per token). Updated Feb 2026.
3
+ * Sources:
4
+ * - Anthropic: https://www.anthropic.com/pricing
5
+ * - OpenAI: https://openai.com/api/pricing
6
+ * - Google: https://ai.google.dev/pricing
7
+ *
8
+ * Note: Not all supported providers have pricing data. Missing providers
9
+ * (Bedrock, Azure, Mistral, etc.) will return 0 from calculateCost().
10
+ */
11
+ const PRICING = {
12
+ // Anthropic (direct API)
13
+ anthropic: {
14
+ "claude-sonnet-4-5-20250929": {
15
+ input: 3.0 / 1_000_000,
16
+ output: 15.0 / 1_000_000,
17
+ cacheRead: 0.3 / 1_000_000,
18
+ cacheCreation: 3.75 / 1_000_000,
19
+ },
20
+ "claude-opus-4-6": {
21
+ input: 15.0 / 1_000_000,
22
+ output: 75.0 / 1_000_000,
23
+ cacheRead: 1.5 / 1_000_000,
24
+ cacheCreation: 18.75 / 1_000_000,
25
+ },
26
+ "claude-haiku-4-5-20251001": {
27
+ input: 0.8 / 1_000_000,
28
+ output: 4.0 / 1_000_000,
29
+ cacheRead: 0.08 / 1_000_000,
30
+ cacheCreation: 1.0 / 1_000_000,
31
+ },
32
+ },
33
+ // Google Vertex AI (same models, same pricing)
34
+ vertex: {
35
+ "claude-sonnet-4-5@20250929": {
36
+ input: 3.0 / 1_000_000,
37
+ output: 15.0 / 1_000_000,
38
+ cacheRead: 0.3 / 1_000_000,
39
+ cacheCreation: 3.75 / 1_000_000,
40
+ },
41
+ "claude-opus-4-6": {
42
+ input: 15.0 / 1_000_000,
43
+ output: 75.0 / 1_000_000,
44
+ cacheRead: 1.5 / 1_000_000,
45
+ cacheCreation: 18.75 / 1_000_000,
46
+ },
47
+ "claude-haiku-4-5@20251001": {
48
+ input: 0.8 / 1_000_000,
49
+ output: 4.0 / 1_000_000,
50
+ cacheRead: 0.08 / 1_000_000,
51
+ cacheCreation: 1.0 / 1_000_000,
52
+ },
53
+ },
54
+ // OpenAI
55
+ openai: {
56
+ "gpt-4o": { input: 2.5 / 1_000_000, output: 10.0 / 1_000_000 },
57
+ "gpt-4o-mini": { input: 0.15 / 1_000_000, output: 0.6 / 1_000_000 },
58
+ "gpt-4-turbo": { input: 10.0 / 1_000_000, output: 30.0 / 1_000_000 },
59
+ o1: { input: 15.0 / 1_000_000, output: 60.0 / 1_000_000 },
60
+ "o1-mini": { input: 1.1 / 1_000_000, output: 4.4 / 1_000_000 },
61
+ },
62
+ // Google (Gemini)
63
+ google: {
64
+ "gemini-2.0-flash": { input: 0.1 / 1_000_000, output: 0.4 / 1_000_000 },
65
+ "gemini-2.0-pro": { input: 1.25 / 1_000_000, output: 10.0 / 1_000_000 },
66
+ "gemini-1.5-pro": { input: 1.25 / 1_000_000, output: 5.0 / 1_000_000 },
67
+ "gemini-1.5-flash": { input: 0.075 / 1_000_000, output: 0.3 / 1_000_000 },
68
+ },
69
+ };
70
+ /**
71
+ * Map of normalized provider aliases to canonical PRICING keys.
72
+ * After stripping non-alpha characters, e.g. "google-ai" becomes "googleai".
73
+ */
74
+ const PROVIDER_ALIASES = {
75
+ googleai: "google",
76
+ googleaistudio: "google",
77
+ anthropic: "anthropic",
78
+ openai: "openai",
79
+ vertex: "vertex",
80
+ google: "google",
81
+ };
82
+ /**
83
+ * Look up per-token rates for a provider/model combination.
84
+ * Normalises the provider name via aliases, then tries an exact model match
85
+ * followed by a longest-prefix match so that e.g. "gpt-4o-2024-08-06"
86
+ * resolves to the "gpt-4o" entry without a false hit on "gpt-4".
87
+ *
88
+ * @returns The rate entry, or undefined when the combination is unknown.
89
+ */
90
+ function findRates(provider, model) {
91
+ const stripped = provider.toLowerCase().replace(/[^a-z]/g, "");
92
+ const normalizedProvider = PROVIDER_ALIASES[stripped] ?? stripped;
93
+ const providerPricing = PRICING[normalizedProvider] || PRICING[provider];
94
+ if (!providerPricing) {
95
+ return undefined;
96
+ }
97
+ // Exact match
98
+ if (providerPricing[model]) {
99
+ return providerPricing[model];
100
+ }
101
+ // Longest-prefix match
102
+ const sortedKeys = Object.keys(providerPricing).sort((a, b) => b.length - a.length);
103
+ const key = sortedKeys.find((k) => model.startsWith(k));
104
+ return key ? providerPricing[key] : undefined;
105
+ }
106
+ /**
107
+ * Calculate the dollar cost of a generate/stream call based on token usage.
108
+ * Returns 0 if the provider/model combination is not in the pricing table.
109
+ */
110
+ export function calculateCost(provider, model, usage) {
111
+ const rates = findRates(provider, model);
112
+ if (!rates) {
113
+ return 0;
114
+ }
115
+ let cost = 0;
116
+ cost += (usage.input || 0) * rates.input;
117
+ cost += (usage.output || 0) * rates.output;
118
+ if (usage.cacheReadTokens && rates.cacheRead) {
119
+ cost += usage.cacheReadTokens * rates.cacheRead;
120
+ }
121
+ if (usage.cacheCreationTokens && rates.cacheCreation) {
122
+ cost += usage.cacheCreationTokens * rates.cacheCreation;
123
+ }
124
+ return Math.round(cost * 1_000_000) / 1_000_000; // Round to 6 decimal places
125
+ }
126
+ /**
127
+ * Check if pricing is available for a provider/model combination.
128
+ * Checks the rate table directly instead of computing a cost,
129
+ * so even very cheap models (e.g. gemini-1.5-flash) are detected correctly.
130
+ */
131
+ export function hasPricing(provider, model) {
132
+ return findRates(provider, model) !== undefined;
133
+ }
134
+ //# sourceMappingURL=pricing.js.map
@@ -5,6 +5,23 @@
5
5
  import { createClient } from "redis";
6
6
  import type { RedisStorageConfig, RedisConversationObject } from "../types/conversation.js";
7
7
  type RedisClient = ReturnType<typeof createClient>;
8
+ /**
9
+ * Get a pooled Redis connection. Multiple callers with the same host:port:db
10
+ * share a single connection, reducing connection count.
11
+ */
12
+ export declare function getPooledRedisClient(config: Required<RedisStorageConfig>): Promise<RedisClient>;
13
+ /**
14
+ * Release a pooled Redis connection. Only closes when refCount reaches 0.
15
+ */
16
+ export declare function releasePooledRedisClient(config: Required<RedisStorageConfig>): Promise<void>;
17
+ /**
18
+ * Get stats about the connection pool
19
+ */
20
+ export declare function getPoolStats(): Array<{
21
+ key: string;
22
+ refCount: number;
23
+ isOpen: boolean;
24
+ }>;
8
25
  /**
9
26
  * Creates a Redis client with the provided configuration
10
27
  */