@juspay/neurolink 9.10.0 → 9.11.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 (174) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/adapters/video/videoAnalyzer.d.ts +3 -3
  3. package/dist/adapters/video/videoAnalyzer.js +39 -25
  4. package/dist/agent/directTools.d.ts +3 -3
  5. package/dist/cli/commands/config.d.ts +9 -9
  6. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  7. package/dist/constants/contextWindows.d.ts +6 -3
  8. package/dist/constants/contextWindows.js +30 -3
  9. package/dist/constants/index.d.ts +3 -3
  10. package/dist/constants/retry.d.ts +4 -4
  11. package/dist/constants/retry.js +1 -1
  12. package/dist/context/contextCompactor.d.ts +1 -1
  13. package/dist/context/contextCompactor.js +59 -1
  14. package/dist/context/summarizationEngine.d.ts +2 -2
  15. package/dist/context/summarizationEngine.js +44 -18
  16. package/dist/context/toolOutputLimits.d.ts +22 -13
  17. package/dist/context/toolOutputLimits.js +58 -64
  18. package/dist/core/baseProvider.d.ts +11 -2
  19. package/dist/core/baseProvider.js +30 -1
  20. package/dist/core/conversationMemoryManager.d.ts +13 -1
  21. package/dist/core/conversationMemoryManager.js +36 -5
  22. package/dist/core/modules/GenerationHandler.d.ts +6 -0
  23. package/dist/core/modules/GenerationHandler.js +192 -7
  24. package/dist/core/modules/MessageBuilder.js +42 -4
  25. package/dist/core/modules/TelemetryHandler.js +4 -1
  26. package/dist/core/redisConversationMemoryManager.d.ts +19 -3
  27. package/dist/core/redisConversationMemoryManager.js +253 -58
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +3 -0
  30. package/dist/lib/adapters/video/videoAnalyzer.d.ts +3 -3
  31. package/dist/lib/adapters/video/videoAnalyzer.js +39 -25
  32. package/dist/lib/agent/directTools.d.ts +7 -7
  33. package/dist/lib/constants/contextWindows.d.ts +6 -3
  34. package/dist/lib/constants/contextWindows.js +30 -3
  35. package/dist/lib/constants/index.d.ts +3 -3
  36. package/dist/lib/constants/retry.d.ts +4 -4
  37. package/dist/lib/constants/retry.js +1 -1
  38. package/dist/lib/context/contextCompactor.d.ts +1 -1
  39. package/dist/lib/context/contextCompactor.js +59 -1
  40. package/dist/lib/context/summarizationEngine.d.ts +2 -2
  41. package/dist/lib/context/summarizationEngine.js +44 -18
  42. package/dist/lib/context/toolOutputLimits.d.ts +22 -13
  43. package/dist/lib/context/toolOutputLimits.js +58 -64
  44. package/dist/lib/core/baseProvider.d.ts +11 -2
  45. package/dist/lib/core/baseProvider.js +30 -1
  46. package/dist/lib/core/conversationMemoryManager.d.ts +13 -1
  47. package/dist/lib/core/conversationMemoryManager.js +36 -5
  48. package/dist/lib/core/modules/GenerationHandler.d.ts +6 -0
  49. package/dist/lib/core/modules/GenerationHandler.js +192 -7
  50. package/dist/lib/core/modules/MessageBuilder.js +42 -4
  51. package/dist/lib/core/modules/TelemetryHandler.js +4 -1
  52. package/dist/lib/core/redisConversationMemoryManager.d.ts +19 -3
  53. package/dist/lib/core/redisConversationMemoryManager.js +253 -58
  54. package/dist/lib/files/fileTools.d.ts +3 -3
  55. package/dist/lib/index.d.ts +2 -0
  56. package/dist/lib/index.js +3 -0
  57. package/dist/lib/mcp/externalServerManager.js +36 -1
  58. package/dist/lib/memory/memoryRetrievalTools.d.ts +166 -0
  59. package/dist/lib/memory/memoryRetrievalTools.js +145 -0
  60. package/dist/lib/neurolink.d.ts +35 -1
  61. package/dist/lib/neurolink.js +471 -16
  62. package/dist/lib/providers/amazonBedrock.d.ts +1 -1
  63. package/dist/lib/providers/amazonBedrock.js +78 -45
  64. package/dist/lib/providers/amazonSagemaker.d.ts +1 -1
  65. package/dist/lib/providers/amazonSagemaker.js +1 -1
  66. package/dist/lib/providers/anthropic.d.ts +1 -1
  67. package/dist/lib/providers/anthropic.js +7 -7
  68. package/dist/lib/providers/anthropicBaseProvider.d.ts +1 -1
  69. package/dist/lib/providers/anthropicBaseProvider.js +7 -6
  70. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  71. package/dist/lib/providers/azureOpenai.js +1 -1
  72. package/dist/lib/providers/googleAiStudio.d.ts +1 -1
  73. package/dist/lib/providers/googleAiStudio.js +5 -5
  74. package/dist/lib/providers/googleVertex.d.ts +1 -1
  75. package/dist/lib/providers/googleVertex.js +74 -17
  76. package/dist/lib/providers/huggingFace.d.ts +1 -1
  77. package/dist/lib/providers/huggingFace.js +1 -1
  78. package/dist/lib/providers/litellm.d.ts +1 -1
  79. package/dist/lib/providers/litellm.js +18 -16
  80. package/dist/lib/providers/mistral.d.ts +1 -1
  81. package/dist/lib/providers/mistral.js +1 -1
  82. package/dist/lib/providers/ollama.d.ts +1 -1
  83. package/dist/lib/providers/ollama.js +8 -7
  84. package/dist/lib/providers/openAI.d.ts +1 -1
  85. package/dist/lib/providers/openAI.js +6 -6
  86. package/dist/lib/providers/openRouter.d.ts +1 -1
  87. package/dist/lib/providers/openRouter.js +6 -2
  88. package/dist/lib/providers/openaiCompatible.d.ts +1 -1
  89. package/dist/lib/providers/openaiCompatible.js +1 -1
  90. package/dist/lib/proxy/proxyFetch.js +291 -65
  91. package/dist/lib/server/utils/validation.d.ts +4 -4
  92. package/dist/lib/services/server/ai/observability/instrumentation.js +12 -3
  93. package/dist/lib/telemetry/telemetryService.d.ts +2 -1
  94. package/dist/lib/telemetry/telemetryService.js +8 -1
  95. package/dist/lib/types/contextTypes.d.ts +26 -2
  96. package/dist/lib/types/conversation.d.ts +72 -40
  97. package/dist/lib/types/conversationMemoryInterface.d.ts +5 -1
  98. package/dist/lib/types/generateTypes.d.ts +26 -0
  99. package/dist/lib/types/modelTypes.d.ts +2 -2
  100. package/dist/lib/types/multimodal.d.ts +2 -0
  101. package/dist/lib/types/observability.d.ts +10 -0
  102. package/dist/lib/types/sdkTypes.d.ts +1 -1
  103. package/dist/lib/utils/conversationMemory.d.ts +4 -3
  104. package/dist/lib/utils/conversationMemory.js +44 -6
  105. package/dist/lib/utils/errorHandling.d.ts +5 -0
  106. package/dist/lib/utils/errorHandling.js +7 -2
  107. package/dist/lib/utils/logger.d.ts +8 -0
  108. package/dist/lib/utils/logger.js +56 -1
  109. package/dist/lib/utils/messageBuilder.js +74 -4
  110. package/dist/lib/utils/redis.js +6 -1
  111. package/dist/lib/utils/tokenEstimation.d.ts +2 -2
  112. package/dist/lib/utils/tokenEstimation.js +16 -1
  113. package/dist/lib/utils/videoAnalysisProcessor.d.ts +2 -1
  114. package/dist/lib/utils/videoAnalysisProcessor.js +7 -2
  115. package/dist/lib/workflow/config.d.ts +110 -110
  116. package/dist/mcp/externalServerManager.js +36 -1
  117. package/dist/memory/memoryRetrievalTools.d.ts +166 -0
  118. package/dist/memory/memoryRetrievalTools.js +144 -0
  119. package/dist/neurolink.d.ts +35 -1
  120. package/dist/neurolink.js +471 -16
  121. package/dist/providers/amazonBedrock.d.ts +1 -1
  122. package/dist/providers/amazonBedrock.js +78 -45
  123. package/dist/providers/amazonSagemaker.d.ts +1 -1
  124. package/dist/providers/amazonSagemaker.js +1 -1
  125. package/dist/providers/anthropic.d.ts +1 -1
  126. package/dist/providers/anthropic.js +7 -7
  127. package/dist/providers/anthropicBaseProvider.d.ts +1 -1
  128. package/dist/providers/anthropicBaseProvider.js +7 -6
  129. package/dist/providers/azureOpenai.d.ts +1 -1
  130. package/dist/providers/azureOpenai.js +1 -1
  131. package/dist/providers/googleAiStudio.d.ts +1 -1
  132. package/dist/providers/googleAiStudio.js +5 -5
  133. package/dist/providers/googleVertex.d.ts +1 -1
  134. package/dist/providers/googleVertex.js +74 -17
  135. package/dist/providers/huggingFace.d.ts +1 -1
  136. package/dist/providers/huggingFace.js +1 -1
  137. package/dist/providers/litellm.d.ts +1 -1
  138. package/dist/providers/litellm.js +18 -16
  139. package/dist/providers/mistral.d.ts +1 -1
  140. package/dist/providers/mistral.js +1 -1
  141. package/dist/providers/ollama.d.ts +1 -1
  142. package/dist/providers/ollama.js +8 -7
  143. package/dist/providers/openAI.d.ts +1 -1
  144. package/dist/providers/openAI.js +6 -6
  145. package/dist/providers/openRouter.d.ts +1 -1
  146. package/dist/providers/openRouter.js +6 -2
  147. package/dist/providers/openaiCompatible.d.ts +1 -1
  148. package/dist/providers/openaiCompatible.js +1 -1
  149. package/dist/proxy/proxyFetch.js +291 -65
  150. package/dist/services/server/ai/observability/instrumentation.js +12 -3
  151. package/dist/telemetry/telemetryService.d.ts +2 -1
  152. package/dist/telemetry/telemetryService.js +8 -1
  153. package/dist/types/contextTypes.d.ts +26 -2
  154. package/dist/types/conversation.d.ts +72 -40
  155. package/dist/types/conversationMemoryInterface.d.ts +5 -1
  156. package/dist/types/generateTypes.d.ts +26 -0
  157. package/dist/types/modelTypes.d.ts +10 -10
  158. package/dist/types/multimodal.d.ts +2 -0
  159. package/dist/types/observability.d.ts +10 -0
  160. package/dist/types/sdkTypes.d.ts +1 -1
  161. package/dist/utils/conversationMemory.d.ts +4 -3
  162. package/dist/utils/conversationMemory.js +44 -6
  163. package/dist/utils/errorHandling.d.ts +5 -0
  164. package/dist/utils/errorHandling.js +7 -2
  165. package/dist/utils/logger.d.ts +8 -0
  166. package/dist/utils/logger.js +56 -1
  167. package/dist/utils/messageBuilder.js +74 -4
  168. package/dist/utils/redis.js +6 -1
  169. package/dist/utils/tokenEstimation.d.ts +2 -2
  170. package/dist/utils/tokenEstimation.js +16 -1
  171. package/dist/utils/videoAnalysisProcessor.d.ts +2 -1
  172. package/dist/utils/videoAnalysisProcessor.js +7 -2
  173. package/dist/workflow/config.d.ts +12 -12
  174. package/package.json +1 -1
@@ -6,36 +6,118 @@
6
6
  import { logger } from "../utils/logger.js";
7
7
  import { shouldBypassProxy } from "./utils/noProxyUtils.js";
8
8
  /**
9
- * Mask credentials in proxy URLs for secure logging
10
- * Replaces user:password@ with [CREDENTIALS_MASKED]@
9
+ * Retry-aware fetch wrapper for transient network errors (ECONNRESET, ETIMEDOUT, socket hang up).
10
+ * Protects all LLM API calls and token refreshes that go through createProxyFetch().
11
11
  */
12
- function maskProxyCredentials(proxyUrl) {
13
- if (!proxyUrl || proxyUrl === "NOT_SET") {
14
- return proxyUrl || "NOT_SET";
12
+ async function fetchWithRetry(url, init, maxRetries = 3, baseDelay = 500) {
13
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
14
+ try {
15
+ return await fetch(url, init);
16
+ }
17
+ catch (error) {
18
+ const err = error;
19
+ const isRetryable = err?.code === "ECONNRESET" ||
20
+ err?.code === "ETIMEDOUT" ||
21
+ err?.message?.includes("socket hang up") ||
22
+ err?.message?.includes("network socket disconnected");
23
+ if (!isRetryable || attempt === maxRetries) {
24
+ throw error;
25
+ }
26
+ const delay = baseDelay * Math.pow(2, attempt);
27
+ logger.debug(`[fetchWithRetry] Transient error (${err?.code || err?.message}), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
28
+ await new Promise((r) => setTimeout(r, delay));
29
+ }
15
30
  }
16
- try {
17
- // Handle URLs with credentials: http://user:password@proxy:port
18
- const credentialPattern = /(:\/\/)([^@:]+):([^@]+)@/;
19
- if (credentialPattern.test(proxyUrl)) {
20
- return proxyUrl.replace(credentialPattern, "$1[CREDENTIALS_MASKED]@");
31
+ throw new Error("fetchWithRetry exhausted"); // unreachable
32
+ }
33
+ /**
34
+ * Parse request body to readable format for debug logging
35
+ */
36
+ function parseBody(body) {
37
+ if (!body) {
38
+ return { parsed: null, size: 0, type: "empty" };
39
+ }
40
+ if (typeof body === "string") {
41
+ try {
42
+ return { parsed: JSON.parse(body), size: body.length, type: "json" };
43
+ }
44
+ catch {
45
+ return { parsed: body, size: body.length, type: "text" };
21
46
  }
22
- // Return original URL if no credentials found
23
- return proxyUrl;
24
47
  }
25
- catch {
26
- // If URL parsing fails, still mask potential credentials pattern
27
- return proxyUrl.replace(/(:\/\/)([^@:]+):([^@]+)@/, "$1[CREDENTIALS_MASKED]@");
48
+ if (body instanceof ArrayBuffer) {
49
+ return {
50
+ parsed: "[ArrayBuffer]",
51
+ size: body.byteLength,
52
+ type: "arraybuffer",
53
+ };
28
54
  }
55
+ if (body instanceof Uint8Array) {
56
+ return { parsed: "[Uint8Array]", size: body.length, type: "uint8array" };
57
+ }
58
+ return { parsed: "[Stream]", size: -1, type: "stream" };
29
59
  }
30
60
  /**
31
- * Mask all proxy credentials in an environment variables object
61
+ * Sensitive header names whose values should be redacted in logs
62
+ */
63
+ const SENSITIVE_HEADERS = new Set([
64
+ "authorization",
65
+ "x-api-key",
66
+ "api-key",
67
+ "x-goog-api-key",
68
+ "proxy-authorization",
69
+ "cookie",
70
+ "set-cookie",
71
+ ]);
72
+ /**
73
+ * Extract all headers as plain object with sensitive values redacted
32
74
  */
33
- function maskProxyEnvVars(envVars) {
34
- const masked = {};
35
- for (const [key, value] of Object.entries(envVars)) {
36
- masked[key] = maskProxyCredentials(value);
75
+ function getAllHeaders(headers) {
76
+ if (!headers) {
77
+ return {};
78
+ }
79
+ const entries = headers instanceof Headers
80
+ ? [...headers.entries()]
81
+ : Array.isArray(headers)
82
+ ? headers
83
+ : Object.entries(headers);
84
+ return Object.fromEntries(entries.map(([key, value]) => SENSITIVE_HEADERS.has(key.toLowerCase())
85
+ ? [key, `${value.substring(0, 4)}***`]
86
+ : [key, value]));
87
+ }
88
+ /**
89
+ * Clone response and read body + headers for debug logging
90
+ */
91
+ async function readResponseBody(response) {
92
+ const headers = {};
93
+ response.headers.forEach((value, key) => {
94
+ headers[key] = SENSITIVE_HEADERS.has(key.toLowerCase())
95
+ ? `${value.substring(0, 4)}***`
96
+ : value;
97
+ });
98
+ try {
99
+ const cloned = response.clone();
100
+ const text = await cloned.text();
101
+ try {
102
+ return {
103
+ parsed: JSON.parse(text),
104
+ size: text.length,
105
+ type: "json",
106
+ headers,
107
+ };
108
+ }
109
+ catch {
110
+ return { parsed: text, size: text.length, type: "text", headers };
111
+ }
112
+ }
113
+ catch {
114
+ return {
115
+ parsed: "[unable to read body]",
116
+ size: -1,
117
+ type: "error",
118
+ headers,
119
+ };
37
120
  }
38
- return masked;
39
121
  }
40
122
  // ==================== LIGHTWEIGHT PROXY IMPLEMENTATIONS ====================
41
123
  // ParsedProxyConfig interface moved to ../types/utilities.js
@@ -61,11 +143,22 @@ function parseProxyUrl(proxyUrl) {
61
143
  return config;
62
144
  }
63
145
  catch (error) {
146
+ // Sanitize proxy URL to avoid leaking credentials in logs/errors
147
+ let safeUrl;
148
+ try {
149
+ const u = new URL(proxyUrl);
150
+ u.username = "";
151
+ u.password = "";
152
+ safeUrl = u.toString();
153
+ }
154
+ catch {
155
+ safeUrl = "[invalid-url]";
156
+ }
64
157
  logger.error("[Proxy] Failed to parse proxy URL", {
65
- proxyUrl: maskProxyCredentials(proxyUrl),
158
+ proxyUrl: safeUrl,
66
159
  error,
67
160
  });
68
- throw new Error(`Invalid proxy URL: ${maskProxyCredentials(proxyUrl)}`);
161
+ throw new Error(`Invalid proxy URL: ${safeUrl}`);
69
162
  }
70
163
  }
71
164
  /**
@@ -160,55 +253,117 @@ export function createProxyFetch() {
160
253
  const allProxy = process.env.ALL_PROXY || process.env.all_proxy;
161
254
  const socksProxy = process.env.SOCKS_PROXY || process.env.socks_proxy;
162
255
  const noProxy = process.env.NO_PROXY || process.env.no_proxy;
163
- // ENHANCED LOGGING: Capture ALL enhanced proxy-related environment variables with credential masking
164
- logger.debug("[Proxy Fetch] 🔍 ENHANCED_PROXY_ENV_DETECTION", {
165
- // Enhanced proxy environment variables (credentials masked)
166
- httpProxy: maskProxyCredentials(httpProxy || "NOT_SET"),
167
- httpsProxy: maskProxyCredentials(httpsProxy || "NOT_SET"),
168
- allProxy: maskProxyCredentials(allProxy || "NOT_SET"),
169
- socksProxy: maskProxyCredentials(socksProxy || "NOT_SET"),
170
- noProxy: noProxy || "NOT_SET", // NO_PROXY doesn't contain credentials
171
- // Legacy variables for compatibility (credentials masked)
172
- originalNodejsHttpProxy: maskProxyCredentials(process.env.nodejs_http_proxy || "NOT_SET"),
173
- originalNodejsHttpsProxy: maskProxyCredentials(process.env.nodejs_https_proxy || "NOT_SET"),
174
- // All potential proxy-related environment variables (credentials masked)
175
- allProxyRelatedEnvVars: maskProxyEnvVars(Object.keys(process.env)
256
+ // ENHANCED LOGGING: Capture ALL proxy-related environment variables credentials redacted
257
+ // Reuse module-level maskProxyUrl, defaulting to "NOT_SET" for undefined values
258
+ const sanitizeProxyUrl = (url) => maskProxyUrl(url) ?? "NOT_SET";
259
+ if (logger.shouldLog("debug")) {
260
+ const allProxyRelatedEnvVars = Object.keys(process.env)
176
261
  .filter((key) => key.toLowerCase().includes("proxy"))
177
262
  .reduce((acc, key) => {
178
- acc[key] = process.env[key] || "NOT_SET";
263
+ const val = process.env[key] || "NOT_SET";
264
+ acc[key] =
265
+ key.toLowerCase() === "no_proxy" ? val : sanitizeProxyUrl(val);
179
266
  return acc;
180
- }, {})),
181
- message: "Enhanced proxy environment detection with SOCKS, authentication, and NO_PROXY support (credentials masked for security)",
182
- });
183
- // If no proxy configured, return standard fetch
267
+ }, {});
268
+ logger.debug("[Proxy Fetch] ENHANCED_PROXY_ENV_DETECTION", {
269
+ httpProxy: sanitizeProxyUrl(httpProxy),
270
+ httpsProxy: sanitizeProxyUrl(httpsProxy),
271
+ allProxy: sanitizeProxyUrl(allProxy),
272
+ socksProxy: sanitizeProxyUrl(socksProxy),
273
+ noProxy: noProxy || "NOT_SET",
274
+ allProxyRelatedEnvVars,
275
+ message: "Enhanced proxy environment detection — credentials redacted",
276
+ });
277
+ }
278
+ // If no proxy configured, return instrumented standard fetch
184
279
  if (!httpsProxy && !httpProxy && !allProxy && !socksProxy) {
185
280
  logger.debug("[Proxy Fetch] No proxy environment variables found - using standard fetch");
186
- return fetch;
281
+ return async (input, init) => {
282
+ const reqId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
283
+ const startTs = Date.now();
284
+ const url = typeof input === "string"
285
+ ? input
286
+ : input instanceof URL
287
+ ? input.href
288
+ : input.url;
289
+ if (logger.shouldLog("debug")) {
290
+ const { parsed: requestBody, size: bodySize, type: bodyType, } = parseBody(init?.body);
291
+ logger.debug("[Observability] HTTP request to LLM provider", {
292
+ requestId: reqId,
293
+ url,
294
+ method: init?.method || "POST",
295
+ headers: getAllHeaders(init?.headers),
296
+ body: requestBody,
297
+ bodySize,
298
+ bodyType,
299
+ });
300
+ }
301
+ try {
302
+ const response = await fetchWithRetry(input, init);
303
+ if (logger.shouldLog("debug")) {
304
+ const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
305
+ logger.debug("[Observability] HTTP response from LLM provider", {
306
+ requestId: reqId,
307
+ url,
308
+ status: response.status,
309
+ statusText: response.statusText,
310
+ durationMs: Date.now() - startTs,
311
+ headers: responseHeaders,
312
+ body: responseBody,
313
+ bodySize: responseSize,
314
+ bodyType: responseType,
315
+ });
316
+ }
317
+ return response;
318
+ }
319
+ catch (error) {
320
+ logger.debug("[Observability] HTTP request failed", {
321
+ requestId: reqId,
322
+ url,
323
+ error: error instanceof Error ? error.message : String(error),
324
+ durationMs: Date.now() - startTs,
325
+ });
326
+ throw error;
327
+ }
328
+ };
187
329
  }
188
330
  logger.debug(`[Proxy Fetch] Configuring enhanced proxy with multiple protocol support`);
189
- logger.debug(`[Proxy Fetch] HTTP_PROXY: ${maskProxyCredentials(httpProxy || "not set")}`);
190
- logger.debug(`[Proxy Fetch] HTTPS_PROXY: ${maskProxyCredentials(httpsProxy || "not set")}`);
191
- logger.debug(`[Proxy Fetch] ALL_PROXY: ${maskProxyCredentials(allProxy || "not set")}`);
192
- logger.debug(`[Proxy Fetch] SOCKS_PROXY: ${maskProxyCredentials(socksProxy || "not set")}`);
331
+ logger.debug(`[Proxy Fetch] HTTP_PROXY: ${sanitizeProxyUrl(httpProxy)}`);
332
+ logger.debug(`[Proxy Fetch] HTTPS_PROXY: ${sanitizeProxyUrl(httpsProxy)}`);
333
+ logger.debug(`[Proxy Fetch] ALL_PROXY: ${sanitizeProxyUrl(allProxy)}`);
334
+ logger.debug(`[Proxy Fetch] SOCKS_PROXY: ${sanitizeProxyUrl(socksProxy)}`);
193
335
  logger.debug(`[Proxy Fetch] NO_PROXY: ${noProxy || "not set"}`);
194
336
  // Return enhanced proxy-aware fetch function
195
337
  return async (input, init) => {
196
338
  const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
339
+ const requestStartTime = Date.now();
197
340
  // Determine target URL
198
341
  const targetUrl = typeof input === "string"
199
342
  ? input
200
343
  : input instanceof URL
201
344
  ? input.href
202
345
  : input.url;
203
- logger.debug(`[Proxy Fetch] 🚀 ENHANCED REQUEST START`, {
346
+ // Request logging with sensitive header redaction — gated behind debug check
347
+ if (logger.shouldLog("debug")) {
348
+ const { parsed: requestBody, size: bodySize, type: bodyType, } = parseBody(init?.body);
349
+ logger.debug("[Observability] HTTP request to LLM provider", {
350
+ requestId,
351
+ url: targetUrl,
352
+ method: init?.method || "POST",
353
+ headers: getAllHeaders(init?.headers),
354
+ body: requestBody,
355
+ bodySize,
356
+ bodyType,
357
+ });
358
+ }
359
+ logger.debug(`[Proxy Fetch] ENHANCED REQUEST START`, {
204
360
  requestId,
205
361
  targetUrl,
206
362
  timestamp: new Date().toISOString(),
207
- httpProxy: httpProxy || "NOT_SET",
208
- httpsProxy: httpsProxy || "NOT_SET",
209
- allProxy: allProxy || "NOT_SET",
210
- socksProxy: socksProxy || "NOT_SET",
211
- initHeaders: init?.headers || "NO_HEADERS",
363
+ httpProxy: sanitizeProxyUrl(httpProxy),
364
+ httpsProxy: sanitizeProxyUrl(httpsProxy),
365
+ allProxy: sanitizeProxyUrl(allProxy),
366
+ socksProxy: sanitizeProxyUrl(socksProxy),
212
367
  initMethod: init?.method || "GET",
213
368
  });
214
369
  try {
@@ -216,18 +371,19 @@ export function createProxyFetch() {
216
371
  const proxyUrl = selectProxyUrl(targetUrl);
217
372
  if (proxyUrl) {
218
373
  const url = new URL(targetUrl);
374
+ const sanitizedProxy = sanitizeProxyUrl(proxyUrl);
219
375
  logger.debug(`[Proxy Fetch] 🔗 ENHANCED URL ANALYSIS`, {
220
376
  requestId,
221
377
  targetUrl,
222
378
  urlHostname: url.hostname,
223
379
  urlProtocol: url.protocol,
224
380
  urlPort: url.port,
225
- selectedProxyUrl: maskProxyCredentials(proxyUrl), // Hide credentials in logs
381
+ selectedProxyUrl: sanitizedProxy,
226
382
  timestamp: new Date().toISOString(),
227
383
  });
228
384
  logger.debug(`[Proxy Fetch] 🎯 ENHANCED PROXY AGENT CREATION`, {
229
385
  requestId,
230
- proxyUrl: maskProxyCredentials(proxyUrl), // Hide credentials
386
+ proxyUrl: sanitizedProxy,
231
387
  targetHostname: url.hostname,
232
388
  targetProtocol: url.protocol,
233
389
  aboutToCreateProxyAgent: true,
@@ -236,8 +392,9 @@ export function createProxyFetch() {
236
392
  // Create/reuse proxy agent (HTTP/HTTPS/SOCKS)
237
393
  const agentCache = globalThis.__NL_PROXY_AGENT_CACHE__ ??
238
394
  (globalThis.__NL_PROXY_AGENT_CACHE__ = new Map());
239
- const dispatcher = agentCache.get(proxyUrl) || (await createProxyAgent(proxyUrl));
240
- agentCache.set(proxyUrl, dispatcher);
395
+ const cacheKey = maskProxyUrl(proxyUrl) ?? proxyUrl; // credentials stripped for key
396
+ const dispatcher = agentCache.get(cacheKey) || (await createProxyAgent(proxyUrl));
397
+ agentCache.set(cacheKey, dispatcher);
241
398
  logger.debug(`[Proxy Fetch] ✅ ENHANCED PROXY AGENT CREATED`, {
242
399
  requestId,
243
400
  hasDispatcher: !!dispatcher,
@@ -266,20 +423,40 @@ export function createProxyFetch() {
266
423
  ...fetchInit,
267
424
  dispatcher: dispatcher,
268
425
  });
269
- logger.debug(`[Proxy Fetch] 🎉 ENHANCED PROXY SUCCESS`, {
426
+ if (logger.shouldLog("debug")) {
427
+ const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
428
+ logger.debug("[Observability] HTTP response from LLM provider", {
429
+ requestId,
430
+ url: targetUrl,
431
+ status: response?.status,
432
+ statusText: response?.statusText,
433
+ durationMs: Date.now() - requestStartTime,
434
+ headers: responseHeaders,
435
+ body: responseBody,
436
+ bodySize: responseSize,
437
+ bodyType: responseType,
438
+ proxied: true,
439
+ });
440
+ }
441
+ logger.debug(`[Proxy Fetch] ENHANCED PROXY SUCCESS`, {
270
442
  requestId,
271
443
  responseStatus: response?.status,
272
444
  responseOk: response?.ok,
273
445
  proxyUsed: true,
274
446
  timestamp: new Date().toISOString(),
275
447
  });
276
- logger.debug(`[Proxy Fetch] ✅ Request proxied successfully via enhanced proxy`);
277
448
  return response;
278
449
  }
279
450
  }
280
451
  catch (error) {
281
452
  const errorMessage = error instanceof Error ? error.message : String(error);
282
- logger.debug(`[Proxy Fetch] 💥 ENHANCED ERROR ANALYSIS`, {
453
+ logger.debug("[Observability] HTTP request failed", {
454
+ requestId,
455
+ url: targetUrl,
456
+ error: errorMessage,
457
+ durationMs: Date.now() - requestStartTime,
458
+ });
459
+ logger.debug(`[Proxy Fetch] ENHANCED ERROR ANALYSIS`, {
283
460
  requestId,
284
461
  error: errorMessage,
285
462
  errorType: error instanceof Error ? error.constructor.name : typeof error,
@@ -289,14 +466,63 @@ export function createProxyFetch() {
289
466
  logger.warn(`[Proxy Fetch] Enhanced proxy failed (${errorMessage}), falling back to direct connection`);
290
467
  }
291
468
  // Fallback to standard fetch
292
- logger.debug(`[Proxy Fetch] 🔄 ENHANCED FALLBACK TO STANDARD FETCH`, {
469
+ logger.debug(`[Proxy Fetch] ENHANCED FALLBACK TO STANDARD FETCH`, {
293
470
  requestId,
294
471
  fallbackReason: "No proxy configured or proxy failed",
295
472
  timestamp: new Date().toISOString(),
296
473
  });
297
- return fetch(input, init);
474
+ try {
475
+ const response = await fetchWithRetry(input, init);
476
+ if (logger.shouldLog("debug")) {
477
+ const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
478
+ logger.debug("[Observability] HTTP response from LLM provider", {
479
+ requestId,
480
+ url: targetUrl,
481
+ status: response.status,
482
+ statusText: response.statusText,
483
+ durationMs: Date.now() - requestStartTime,
484
+ headers: responseHeaders,
485
+ body: responseBody,
486
+ bodySize: responseSize,
487
+ bodyType: responseType,
488
+ proxied: false,
489
+ });
490
+ }
491
+ return response;
492
+ }
493
+ catch (fallbackError) {
494
+ const fallbackMessage = fallbackError instanceof Error
495
+ ? fallbackError.message
496
+ : String(fallbackError);
497
+ logger.debug("[Observability] HTTP request failed", {
498
+ requestId,
499
+ url: targetUrl,
500
+ error: fallbackMessage,
501
+ durationMs: Date.now() - requestStartTime,
502
+ });
503
+ throw fallbackError;
504
+ }
298
505
  };
299
506
  }
507
+ /**
508
+ * Mask credentials in a proxy URL for safe logging/reporting.
509
+ */
510
+ function maskProxyUrl(url) {
511
+ if (!url) {
512
+ return null;
513
+ }
514
+ try {
515
+ const u = new URL(url);
516
+ if (u.username || u.password) {
517
+ u.username = "***";
518
+ u.password = "***";
519
+ }
520
+ return u.toString();
521
+ }
522
+ catch {
523
+ return "[invalid-url]";
524
+ }
525
+ }
300
526
  /**
301
527
  * Get enhanced proxy status information
302
528
  */
@@ -308,10 +534,10 @@ export function getProxyStatus() {
308
534
  const noProxy = process.env.NO_PROXY || process.env.no_proxy;
309
535
  return {
310
536
  enabled: !!(httpsProxy || httpProxy || allProxy || socksProxy),
311
- httpProxy: httpProxy || null,
312
- httpsProxy: httpsProxy || null,
313
- allProxy: allProxy || null,
314
- socksProxy: socksProxy || null,
537
+ httpProxy: maskProxyUrl(httpProxy),
538
+ httpsProxy: maskProxyUrl(httpsProxy),
539
+ allProxy: maskProxyUrl(allProxy),
540
+ socksProxy: maskProxyUrl(socksProxy),
315
541
  noProxy: noProxy || null,
316
542
  method: "enhanced-proxy-agent",
317
543
  capabilities: [
@@ -394,11 +394,20 @@ export function initializeOpenTelemetry(config) {
394
394
  const provider = globalProvider;
395
395
  // Add ContextEnricher for trace name enrichment
396
396
  provider.addSpanProcessor(new ContextEnricher());
397
- // Also add LangfuseSpanProcessor so traces are sent to Langfuse
398
- provider.addSpanProcessor(langfuseProcessor);
397
+ // Only add LangfuseSpanProcessor if the host has not already registered one.
398
+ // When skipLangfuseSpanProcessor is true, the host (e.g. Curator) already
399
+ // registers its own LangfuseSpanProcessor via a DeferredSpanProcessor, so
400
+ // adding another one here would cause duplicate trace exports to Langfuse.
401
+ const skipLangfuse = config.skipLangfuseSpanProcessor === true;
402
+ if (!skipLangfuse) {
403
+ provider.addSpanProcessor(langfuseProcessor);
404
+ }
399
405
  logger.info(`${LOG_PREFIX} Auto-registered processors with global TracerProvider`, {
400
- processors: ["ContextEnricher", "LangfuseSpanProcessor"],
406
+ processors: skipLangfuse
407
+ ? ["ContextEnricher"]
408
+ : ["ContextEnricher", "LangfuseSpanProcessor"],
401
409
  reason: "External provider mode with auto-registration",
410
+ skippedLangfuseSpanProcessor: skipLangfuse,
402
411
  });
403
412
  }
404
413
  else {
@@ -17,6 +17,7 @@ export declare class TelemetryService {
17
17
  private aiRequestDuration?;
18
18
  private aiTokensUsed?;
19
19
  private aiProviderErrors?;
20
+ private aiCostUsd?;
20
21
  private mcpToolCalls?;
21
22
  private connectionCounter?;
22
23
  private responseTimeHistogram?;
@@ -32,7 +33,7 @@ export declare class TelemetryService {
32
33
  private initializeMetrics;
33
34
  initialize(): Promise<void>;
34
35
  traceAIRequest<T>(provider: string, operation: () => Promise<T>, operationType?: string): Promise<T>;
35
- recordAIRequest(provider: string, model: string, tokens: number, duration: number): void;
36
+ recordAIRequest(provider: string, model: string, tokens: number, duration: number, cost?: number): void;
36
37
  recordAIError(provider: string, error: Error): void;
37
38
  recordMCPToolCall(toolName: string, duration: number, success: boolean): void;
38
39
  recordConnection(type: "websocket" | "sse" | "http"): void;
@@ -16,6 +16,7 @@ export class TelemetryService {
16
16
  aiRequestDuration;
17
17
  aiTokensUsed;
18
18
  aiProviderErrors;
19
+ aiCostUsd;
19
20
  mcpToolCalls;
20
21
  connectionCounter;
21
22
  responseTimeHistogram;
@@ -79,6 +80,9 @@ export class TelemetryService {
79
80
  this.aiTokensUsed = this.meter.createCounter("ai_tokens_used_total", {
80
81
  description: "Total number of AI tokens used",
81
82
  });
83
+ this.aiCostUsd = this.meter.createCounter("ai_cost_usd_total", {
84
+ description: "Total accumulated AI cost in USD",
85
+ });
82
86
  this.aiProviderErrors = this.meter.createCounter("ai_provider_errors_total", {
83
87
  description: "Total number of AI provider errors",
84
88
  });
@@ -136,7 +140,7 @@ export class TelemetryService {
136
140
  }
137
141
  }
138
142
  // Metrics Recording (NO-OP when disabled)
139
- recordAIRequest(provider, model, tokens, duration) {
143
+ recordAIRequest(provider, model, tokens, duration, cost) {
140
144
  // Track runtime metrics
141
145
  this.requestCount++;
142
146
  this.totalResponseTime += duration;
@@ -148,6 +152,9 @@ export class TelemetryService {
148
152
  this.aiRequestCounter.add(1, labels);
149
153
  this.aiRequestDuration?.record(duration, labels);
150
154
  this.aiTokensUsed?.add(tokens, labels);
155
+ if (cost !== undefined && Number.isFinite(cost) && cost > 0) {
156
+ this.aiCostUsd?.add(cost, labels);
157
+ }
151
158
  }
152
159
  recordAIError(provider, error) {
153
160
  // Track runtime metrics
@@ -343,7 +343,9 @@ export type BudgetFileInput = {
343
343
  /** Optional file type hint for type-aware token estimation */
344
344
  fileType?: string;
345
345
  };
346
- /** Options for tool output truncation. */
346
+ /**
347
+ * @deprecated Use ToolOutputPreviewOptions instead.
348
+ */
347
349
  export type TruncateOptions = {
348
350
  maxBytes?: number;
349
351
  maxLines?: number;
@@ -351,13 +353,35 @@ export type TruncateOptions = {
351
353
  saveToDisk?: boolean;
352
354
  saveDir?: string;
353
355
  };
354
- /** Result of tool output truncation. */
356
+ /**
357
+ * @deprecated Use ToolOutputPreviewResult instead.
358
+ */
355
359
  export type TruncateResult = {
356
360
  content: string;
357
361
  truncated: boolean;
358
362
  savedPath?: string;
359
363
  originalSize: number;
360
364
  };
365
+ /** Options for tool output preview generation. */
366
+ export type ToolOutputPreviewOptions = {
367
+ /** Maximum bytes for the preview (default: 50KB) */
368
+ maxBytes?: number;
369
+ /** Maximum lines for the preview (default: 2000) */
370
+ maxLines?: number;
371
+ /** Fraction of preview budget allocated to the head (default: 0.25) */
372
+ headRatio?: number;
373
+ /** Fraction of preview budget allocated to the tail (default: 0.75) */
374
+ tailRatio?: number;
375
+ };
376
+ /** Result of tool output preview generation. */
377
+ export type ToolOutputPreviewResult = {
378
+ /** The preview string (or full output if under limits) */
379
+ preview: string;
380
+ /** Whether truncation was applied */
381
+ truncated: boolean;
382
+ /** Original byte size of the full output */
383
+ originalSize: number;
384
+ };
361
385
  /** Result of tool pair repair. */
362
386
  export type RepairResult = {
363
387
  repaired: boolean;