@juspay/neurolink 10.7.1 → 10.8.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 (34) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/adapters/replicate/predictionLifecycle.d.ts +1 -1
  3. package/dist/adapters/replicate/predictionLifecycle.js +50 -45
  4. package/dist/agent/directTools.d.ts +2 -2
  5. package/dist/browser/neurolink.min.js +373 -373
  6. package/dist/core/baseProvider.d.ts +1 -0
  7. package/dist/core/baseProvider.js +74 -11
  8. package/dist/core/modules/TelemetryHandler.d.ts +5 -1
  9. package/dist/core/modules/TelemetryHandler.js +20 -0
  10. package/dist/lib/adapters/replicate/predictionLifecycle.d.ts +1 -1
  11. package/dist/lib/adapters/replicate/predictionLifecycle.js +50 -45
  12. package/dist/lib/core/baseProvider.d.ts +1 -0
  13. package/dist/lib/core/baseProvider.js +74 -11
  14. package/dist/lib/core/modules/TelemetryHandler.d.ts +5 -1
  15. package/dist/lib/core/modules/TelemetryHandler.js +20 -0
  16. package/dist/lib/mcp/httpRateLimiter.js +14 -21
  17. package/dist/lib/types/generate.d.ts +16 -0
  18. package/dist/lib/utils/errorHandling.d.ts +2 -0
  19. package/dist/lib/utils/errorHandling.js +2 -0
  20. package/dist/lib/utils/pdfProcessor.js +2 -2
  21. package/dist/lib/utils/providerRetry.d.ts +5 -1
  22. package/dist/lib/utils/providerRetry.js +34 -8
  23. package/dist/lib/utils/retryAfter.d.ts +7 -0
  24. package/dist/lib/utils/retryAfter.js +44 -0
  25. package/dist/mcp/httpRateLimiter.js +14 -21
  26. package/dist/types/generate.d.ts +16 -0
  27. package/dist/utils/errorHandling.d.ts +2 -0
  28. package/dist/utils/errorHandling.js +2 -0
  29. package/dist/utils/pdfProcessor.js +2 -2
  30. package/dist/utils/providerRetry.d.ts +5 -1
  31. package/dist/utils/providerRetry.js +34 -8
  32. package/dist/utils/retryAfter.d.ts +7 -0
  33. package/dist/utils/retryAfter.js +43 -0
  34. package/package.json +3 -2
@@ -220,6 +220,7 @@ export declare abstract class BaseProvider implements AIProvider {
220
220
  private handleVideoFrameGeneration;
221
221
  private executeStandardGenerateFlow;
222
222
  protected synthesizeAIResponseIfNeeded(enhancedResult: EnhancedGenerateResult, options: TextGenerationOptions): Promise<EnhancedGenerateResult>;
223
+ private getTTSErrorDetails;
223
224
  /**
224
225
  * BACKWARD COMPATIBILITY: Legacy generateText method
225
226
  * Converts EnhancedGenerateResult to TextGenerationResult format
@@ -4,16 +4,16 @@ import { IMAGE_GENERATION_MODELS } from "../core/constants.js";
4
4
  import { MiddlewareFactory } from "../middleware/factory.js";
5
5
  import { modelSupports } from "../models/modelRegistry.js";
6
6
  import { ATTR, tracers } from "../telemetry/index.js";
7
- import { isAbortError } from "../utils/errorHandling.js";
7
+ import { isAbortError, NeuroLinkError } from "../utils/errorHandling.js";
8
8
  import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "../utils/lifecycleCallbacks.js";
9
9
  import { resolveLifecycleTimeoutMs } from "../utils/lifecycleTimeout.js";
10
10
  import { logger } from "../utils/logger.js";
11
- import { withTimeoutFn } from "../utils/async/withTimeout.js";
11
+ import { TimeoutError as AsyncTimeoutError, withTimeoutFn, } from "../utils/async/withTimeout.js";
12
12
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
13
13
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
14
14
  import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
15
15
  import { ToolExecutionRecorder, resolveToolExecutionRecords, } from "./toolExecutionRecorder.js";
16
- import { TTSProcessor } from "../utils/ttsProcessor.js";
16
+ import { TTS_ERROR_CODES, TTSProcessor } from "../utils/ttsProcessor.js";
17
17
  import { executeVideoAnalysis, hasVideoFrames, } from "../utils/videoAnalysisProcessor.js";
18
18
  import { dedupeTools } from "./toolDedup.js";
19
19
  import { resolveToolPolicy, toolNameMatcher } from "../tools/toolPolicy.js";
@@ -1016,13 +1016,31 @@ export class BaseProvider {
1016
1016
  model: this.modelName,
1017
1017
  usage: { input: 0, output: 0, total: 0 },
1018
1018
  };
1019
+ const ttsOptions = options.tts;
1020
+ if (!ttsOptions) {
1021
+ return this.enhanceResult(baseResult, options, startTime);
1022
+ }
1023
+ const ttsStartTime = Date.now();
1024
+ const ttsProvider = ttsOptions.provider ?? options.provider ?? this.providerName;
1025
+ const ttsTimeout = this.getTimeout(options);
1019
1026
  try {
1020
- if (!options.tts) {
1021
- return this.enhanceResult(baseResult, options, startTime);
1022
- }
1023
- baseResult.audio = await TTSProcessor.synthesize(textToSynthesize, options.tts.provider ?? options.provider ?? this.providerName, options.tts);
1027
+ baseResult.audio = await withTimeoutFn(() => TTSProcessor.synthesize(textToSynthesize, ttsProvider, ttsOptions), ttsTimeout, `TTS synthesis timed out after ${ttsTimeout}ms for provider "${ttsProvider}"`);
1028
+ baseResult.ttsMetadata = {
1029
+ attempted: true,
1030
+ success: true,
1031
+ latency: Date.now() - ttsStartTime,
1032
+ };
1024
1033
  }
1025
1034
  catch (ttsError) {
1035
+ const latency = Date.now() - ttsStartTime;
1036
+ const error = this.getTTSErrorDetails(ttsError);
1037
+ baseResult.ttsMetadata = {
1038
+ attempted: true,
1039
+ success: false,
1040
+ error,
1041
+ latency,
1042
+ };
1043
+ this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
1026
1044
  logger.error(`TTS synthesis failed in Mode 1 (direct input synthesis):`, ttsError);
1027
1045
  }
1028
1046
  return this.enhanceResult(baseResult, options, startTime);
@@ -1144,8 +1162,9 @@ export class BaseProvider {
1144
1162
  if (!options.tts?.enabled || !options.tts?.useAiResponse) {
1145
1163
  return enhancedResult;
1146
1164
  }
1165
+ const ttsOptions = options.tts;
1147
1166
  const aiResponse = enhancedResult.content;
1148
- const ttsProvider = options.tts?.provider ?? options.provider ?? this.providerName;
1167
+ const ttsProvider = ttsOptions.provider ?? options.provider ?? this.providerName;
1149
1168
  if (!aiResponse || !ttsProvider) {
1150
1169
  logger.warn(`TTS synthesis skipped despite being enabled`, {
1151
1170
  provider: this.providerName,
@@ -1160,19 +1179,63 @@ export class BaseProvider {
1160
1179
  ? "AI response is empty or undefined"
1161
1180
  : "Provider is missing",
1162
1181
  });
1163
- return enhancedResult;
1182
+ return {
1183
+ ...enhancedResult,
1184
+ ttsMetadata: {
1185
+ attempted: false,
1186
+ success: false,
1187
+ },
1188
+ };
1164
1189
  }
1190
+ const ttsStartTime = Date.now();
1191
+ const ttsTimeout = this.getTimeout(options);
1165
1192
  try {
1166
- const ttsResult = await TTSProcessor.synthesize(aiResponse, ttsProvider, options.tts);
1193
+ const ttsResult = await withTimeoutFn(() => TTSProcessor.synthesize(aiResponse, ttsProvider, ttsOptions), ttsTimeout, `TTS synthesis timed out after ${ttsTimeout}ms for provider "${ttsProvider}"`);
1167
1194
  return {
1168
1195
  ...enhancedResult,
1169
1196
  audio: ttsResult,
1197
+ ttsMetadata: {
1198
+ attempted: true,
1199
+ success: true,
1200
+ latency: Date.now() - ttsStartTime,
1201
+ },
1170
1202
  };
1171
1203
  }
1172
1204
  catch (ttsError) {
1205
+ const latency = Date.now() - ttsStartTime;
1206
+ const error = this.getTTSErrorDetails(ttsError);
1207
+ this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
1173
1208
  logger.error(`TTS synthesis failed in Mode 2 (AI response synthesis):`, ttsError);
1174
- return enhancedResult;
1209
+ return {
1210
+ ...enhancedResult,
1211
+ ttsMetadata: {
1212
+ attempted: true,
1213
+ success: false,
1214
+ error,
1215
+ latency,
1216
+ },
1217
+ };
1218
+ }
1219
+ }
1220
+ getTTSErrorDetails(error) {
1221
+ if (error instanceof AsyncTimeoutError) {
1222
+ return {
1223
+ code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
1224
+ message: error.message,
1225
+ retriable: true,
1226
+ };
1227
+ }
1228
+ if (error instanceof NeuroLinkError) {
1229
+ return {
1230
+ code: error.code,
1231
+ message: error.message,
1232
+ retriable: error.retriable,
1233
+ };
1175
1234
  }
1235
+ return {
1236
+ code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
1237
+ message: error instanceof Error ? error.message : String(error),
1238
+ };
1176
1239
  }
1177
1240
  /**
1178
1241
  * BACKWARD COMPATIBILITY: Legacy generateText method
@@ -14,7 +14,7 @@
14
14
  * @module core/modules/TelemetryHandler
15
15
  */
16
16
  import type { NeuroLink } from "../../neurolink.js";
17
- import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, RawUsageObject, TextGenerationOptions, TokenUsage } from "../../types/index.js";
17
+ import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, RawUsageObject, TextGenerationOptions, TokenUsage, TTSMetadata } from "../../types/index.js";
18
18
  /**
19
19
  * TelemetryHandler class - Handles analytics and telemetry for AI providers
20
20
  */
@@ -35,6 +35,10 @@ export declare class TelemetryHandler {
35
35
  * Record performance metrics for a generation
36
36
  */
37
37
  recordPerformanceMetrics(usage: RawUsageObject | undefined, responseTime: number): Promise<void>;
38
+ /**
39
+ * Record a TTS synthesis failure without affecting the generation result.
40
+ */
41
+ recordTTSFailure(ttsProvider: string, error: NonNullable<TTSMetadata["error"]>, latency: number): void;
38
42
  /**
39
43
  * Calculate actual cost based on token usage and provider configuration.
40
44
  *
@@ -100,6 +100,26 @@ export class TelemetryHandler {
100
100
  logger.warn("⚠️ Performance recording failed:", perfError);
101
101
  }
102
102
  }
103
+ /**
104
+ * Record a TTS synthesis failure without affecting the generation result.
105
+ */
106
+ recordTTSFailure(ttsProvider, error, latency) {
107
+ try {
108
+ const labels = {
109
+ provider: ttsProvider,
110
+ error_code: error.code,
111
+ ...(error.retriable !== undefined
112
+ ? { retriable: error.retriable.toString() }
113
+ : {}),
114
+ };
115
+ const telemetry = TelemetryService.getInstance();
116
+ telemetry.recordCustomMetric("tts_failures", 1, labels);
117
+ telemetry.recordCustomHistogram("tts_failure_latency_ms", latency, labels);
118
+ }
119
+ catch (telemetryError) {
120
+ logger.warn("TTS failure telemetry recording failed:", telemetryError);
121
+ }
122
+ }
103
123
  /**
104
124
  * Calculate actual cost based on token usage and provider configuration.
105
125
  *
@@ -18,7 +18,7 @@ import type { ReplicateAuth, ReplicateCreatePredictionInput, ReplicatePollOption
18
18
  * Submit a Replicate prediction. Uses `Prefer: wait=60` so quick
19
19
  * predictions complete in the initial POST and skip polling entirely.
20
20
  */
21
- export declare function createPrediction(auth: ReplicateAuth, input: ReplicateCreatePredictionInput): Promise<ReplicatePrediction>;
21
+ export declare function createPrediction(auth: ReplicateAuth, input: ReplicateCreatePredictionInput, sleep?: (delayMs: number) => Promise<void>): Promise<ReplicatePrediction>;
22
22
  /**
23
23
  * Poll a Replicate prediction until it reaches a terminal status
24
24
  * (succeeded / failed / canceled) or the total timeout elapses.
@@ -17,6 +17,8 @@ import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
17
17
  import { logger } from "../../utils/logger.js";
18
18
  import { NeuroLinkError, ERROR_CODES } from "../../utils/errorHandling.js";
19
19
  import { sanitizeForLog } from "../../utils/logSanitize.js";
20
+ import { withProviderRetry } from "../../utils/providerRetry.js";
21
+ import { parseRetryAfterMs } from "../../utils/retryAfter.js";
20
22
  import { safeDownload } from "../../utils/safeFetch.js";
21
23
  import { MAX_VIDEO_BYTES } from "../../utils/sizeGuard.js";
22
24
  // The submit path sends `Prefer: wait=60`, so Replicate can legitimately
@@ -31,7 +33,7 @@ const DEFAULT_TOTAL_TIMEOUT_MS = 5 * 60_000;
31
33
  * Submit a Replicate prediction. Uses `Prefer: wait=60` so quick
32
34
  * predictions complete in the initial POST and skip polling entirely.
33
35
  */
34
- export async function createPrediction(auth, input) {
36
+ export async function createPrediction(auth, input, sleep) {
35
37
  const baseUrl = auth.baseUrl ?? "https://api.replicate.com";
36
38
  const [modelPath, version] = input.model.split(":", 2);
37
39
  const endpoint = version
@@ -46,51 +48,53 @@ export async function createPrediction(auth, input) {
46
48
  if (input.webhookEventsFilter && input.webhookEventsFilter.length > 0) {
47
49
  body.webhook_events_filter = input.webhookEventsFilter;
48
50
  }
49
- const controller = new AbortController();
50
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
51
- let response;
52
- try {
53
- response = await fetch(endpoint, {
54
- method: "POST",
55
- headers: {
56
- Authorization: `Token ${auth.apiToken}`,
57
- "Content-Type": "application/json",
58
- Prefer: "wait=60",
59
- },
60
- body: JSON.stringify(body),
61
- signal: controller.signal,
62
- });
63
- }
64
- catch (err) {
65
- if (err instanceof NeuroLinkError) {
66
- throw err;
67
- }
68
- if (err instanceof Error && err.name === "AbortError") {
69
- throw new NeuroLinkError({
70
- code: ERROR_CODES.OPERATION_ABORTED,
71
- message: `Replicate predictions submit timed out after ${REQUEST_TIMEOUT_MS / 1000}s`,
72
- category: ErrorCategory.TIMEOUT,
73
- severity: ErrorSeverity.HIGH,
74
- retriable: true,
75
- originalError: err,
51
+ return withProviderRetry(async () => {
52
+ const controller = new AbortController();
53
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
54
+ try {
55
+ const response = await fetch(endpoint, {
56
+ method: "POST",
57
+ headers: {
58
+ Authorization: `Token ${auth.apiToken}`,
59
+ "Content-Type": "application/json",
60
+ Prefer: "wait=60",
61
+ },
62
+ body: JSON.stringify(body),
63
+ signal: controller.signal,
76
64
  });
65
+ if (!response.ok) {
66
+ const raw = await response.text();
67
+ throw new NeuroLinkError({
68
+ code: ERROR_CODES.PROVIDER_NOT_AVAILABLE,
69
+ message: `Replicate predictions submit failed: ${response.status} — ${sanitizeForLog(raw, 500)}`,
70
+ category: ErrorCategory.NETWORK,
71
+ severity: ErrorSeverity.HIGH,
72
+ retriable: response.status >= 500 || response.status === 429,
73
+ retryAfterMs: parseRetryAfterMs(response.headers),
74
+ });
75
+ }
76
+ return (await response.json());
77
77
  }
78
- throw err;
79
- }
80
- finally {
81
- clearTimeout(timeoutId);
82
- }
83
- if (!response.ok) {
84
- const raw = await response.text();
85
- throw new NeuroLinkError({
86
- code: ERROR_CODES.PROVIDER_NOT_AVAILABLE,
87
- message: `Replicate predictions submit failed: ${response.status} — ${sanitizeForLog(raw, 500)}`,
88
- category: ErrorCategory.NETWORK,
89
- severity: ErrorSeverity.HIGH,
90
- retriable: response.status >= 500,
91
- });
92
- }
93
- return (await response.json());
78
+ catch (error) {
79
+ if (error instanceof NeuroLinkError) {
80
+ throw error;
81
+ }
82
+ if (error instanceof Error && error.name === "AbortError") {
83
+ throw new NeuroLinkError({
84
+ code: ERROR_CODES.OPERATION_ABORTED,
85
+ message: `Replicate predictions submit timed out after ${REQUEST_TIMEOUT_MS / 1000}s`,
86
+ category: ErrorCategory.TIMEOUT,
87
+ severity: ErrorSeverity.HIGH,
88
+ retriable: true,
89
+ originalError: error,
90
+ });
91
+ }
92
+ throw error;
93
+ }
94
+ finally {
95
+ clearTimeout(timeoutId);
96
+ }
97
+ }, undefined, "Replicate prediction submission", sleep);
94
98
  }
95
99
  /**
96
100
  * Poll a Replicate prediction until it reaches a terminal status
@@ -164,7 +168,8 @@ export async function pollPrediction(auth, predictionId, options = {}) {
164
168
  message: `Replicate poll failed: ${response.status} — ${sanitizeForLog(raw, 500)}`,
165
169
  category: ErrorCategory.NETWORK,
166
170
  severity: ErrorSeverity.HIGH,
167
- retriable: response.status >= 500,
171
+ retriable: response.status >= 500 || response.status === 429,
172
+ retryAfterMs: parseRetryAfterMs(response.headers),
168
173
  });
169
174
  }
170
175
  const pred = (await response.json());
@@ -220,6 +220,7 @@ export declare abstract class BaseProvider implements AIProvider {
220
220
  private handleVideoFrameGeneration;
221
221
  private executeStandardGenerateFlow;
222
222
  protected synthesizeAIResponseIfNeeded(enhancedResult: EnhancedGenerateResult, options: TextGenerationOptions): Promise<EnhancedGenerateResult>;
223
+ private getTTSErrorDetails;
223
224
  /**
224
225
  * BACKWARD COMPATIBILITY: Legacy generateText method
225
226
  * Converts EnhancedGenerateResult to TextGenerationResult format
@@ -4,16 +4,16 @@ import { IMAGE_GENERATION_MODELS } from "../core/constants.js";
4
4
  import { MiddlewareFactory } from "../middleware/factory.js";
5
5
  import { modelSupports } from "../models/modelRegistry.js";
6
6
  import { ATTR, tracers } from "../telemetry/index.js";
7
- import { isAbortError } from "../utils/errorHandling.js";
7
+ import { isAbortError, NeuroLinkError } from "../utils/errorHandling.js";
8
8
  import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "../utils/lifecycleCallbacks.js";
9
9
  import { resolveLifecycleTimeoutMs } from "../utils/lifecycleTimeout.js";
10
10
  import { logger } from "../utils/logger.js";
11
- import { withTimeoutFn } from "../utils/async/withTimeout.js";
11
+ import { TimeoutError as AsyncTimeoutError, withTimeoutFn, } from "../utils/async/withTimeout.js";
12
12
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
13
13
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
14
14
  import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
15
15
  import { ToolExecutionRecorder, resolveToolExecutionRecords, } from "./toolExecutionRecorder.js";
16
- import { TTSProcessor } from "../utils/ttsProcessor.js";
16
+ import { TTS_ERROR_CODES, TTSProcessor } from "../utils/ttsProcessor.js";
17
17
  import { executeVideoAnalysis, hasVideoFrames, } from "../utils/videoAnalysisProcessor.js";
18
18
  import { dedupeTools } from "./toolDedup.js";
19
19
  import { resolveToolPolicy, toolNameMatcher } from "../tools/toolPolicy.js";
@@ -1016,13 +1016,31 @@ export class BaseProvider {
1016
1016
  model: this.modelName,
1017
1017
  usage: { input: 0, output: 0, total: 0 },
1018
1018
  };
1019
+ const ttsOptions = options.tts;
1020
+ if (!ttsOptions) {
1021
+ return this.enhanceResult(baseResult, options, startTime);
1022
+ }
1023
+ const ttsStartTime = Date.now();
1024
+ const ttsProvider = ttsOptions.provider ?? options.provider ?? this.providerName;
1025
+ const ttsTimeout = this.getTimeout(options);
1019
1026
  try {
1020
- if (!options.tts) {
1021
- return this.enhanceResult(baseResult, options, startTime);
1022
- }
1023
- baseResult.audio = await TTSProcessor.synthesize(textToSynthesize, options.tts.provider ?? options.provider ?? this.providerName, options.tts);
1027
+ baseResult.audio = await withTimeoutFn(() => TTSProcessor.synthesize(textToSynthesize, ttsProvider, ttsOptions), ttsTimeout, `TTS synthesis timed out after ${ttsTimeout}ms for provider "${ttsProvider}"`);
1028
+ baseResult.ttsMetadata = {
1029
+ attempted: true,
1030
+ success: true,
1031
+ latency: Date.now() - ttsStartTime,
1032
+ };
1024
1033
  }
1025
1034
  catch (ttsError) {
1035
+ const latency = Date.now() - ttsStartTime;
1036
+ const error = this.getTTSErrorDetails(ttsError);
1037
+ baseResult.ttsMetadata = {
1038
+ attempted: true,
1039
+ success: false,
1040
+ error,
1041
+ latency,
1042
+ };
1043
+ this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
1026
1044
  logger.error(`TTS synthesis failed in Mode 1 (direct input synthesis):`, ttsError);
1027
1045
  }
1028
1046
  return this.enhanceResult(baseResult, options, startTime);
@@ -1144,8 +1162,9 @@ export class BaseProvider {
1144
1162
  if (!options.tts?.enabled || !options.tts?.useAiResponse) {
1145
1163
  return enhancedResult;
1146
1164
  }
1165
+ const ttsOptions = options.tts;
1147
1166
  const aiResponse = enhancedResult.content;
1148
- const ttsProvider = options.tts?.provider ?? options.provider ?? this.providerName;
1167
+ const ttsProvider = ttsOptions.provider ?? options.provider ?? this.providerName;
1149
1168
  if (!aiResponse || !ttsProvider) {
1150
1169
  logger.warn(`TTS synthesis skipped despite being enabled`, {
1151
1170
  provider: this.providerName,
@@ -1160,19 +1179,63 @@ export class BaseProvider {
1160
1179
  ? "AI response is empty or undefined"
1161
1180
  : "Provider is missing",
1162
1181
  });
1163
- return enhancedResult;
1182
+ return {
1183
+ ...enhancedResult,
1184
+ ttsMetadata: {
1185
+ attempted: false,
1186
+ success: false,
1187
+ },
1188
+ };
1164
1189
  }
1190
+ const ttsStartTime = Date.now();
1191
+ const ttsTimeout = this.getTimeout(options);
1165
1192
  try {
1166
- const ttsResult = await TTSProcessor.synthesize(aiResponse, ttsProvider, options.tts);
1193
+ const ttsResult = await withTimeoutFn(() => TTSProcessor.synthesize(aiResponse, ttsProvider, ttsOptions), ttsTimeout, `TTS synthesis timed out after ${ttsTimeout}ms for provider "${ttsProvider}"`);
1167
1194
  return {
1168
1195
  ...enhancedResult,
1169
1196
  audio: ttsResult,
1197
+ ttsMetadata: {
1198
+ attempted: true,
1199
+ success: true,
1200
+ latency: Date.now() - ttsStartTime,
1201
+ },
1170
1202
  };
1171
1203
  }
1172
1204
  catch (ttsError) {
1205
+ const latency = Date.now() - ttsStartTime;
1206
+ const error = this.getTTSErrorDetails(ttsError);
1207
+ this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
1173
1208
  logger.error(`TTS synthesis failed in Mode 2 (AI response synthesis):`, ttsError);
1174
- return enhancedResult;
1209
+ return {
1210
+ ...enhancedResult,
1211
+ ttsMetadata: {
1212
+ attempted: true,
1213
+ success: false,
1214
+ error,
1215
+ latency,
1216
+ },
1217
+ };
1218
+ }
1219
+ }
1220
+ getTTSErrorDetails(error) {
1221
+ if (error instanceof AsyncTimeoutError) {
1222
+ return {
1223
+ code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
1224
+ message: error.message,
1225
+ retriable: true,
1226
+ };
1227
+ }
1228
+ if (error instanceof NeuroLinkError) {
1229
+ return {
1230
+ code: error.code,
1231
+ message: error.message,
1232
+ retriable: error.retriable,
1233
+ };
1175
1234
  }
1235
+ return {
1236
+ code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
1237
+ message: error instanceof Error ? error.message : String(error),
1238
+ };
1176
1239
  }
1177
1240
  /**
1178
1241
  * BACKWARD COMPATIBILITY: Legacy generateText method
@@ -14,7 +14,7 @@
14
14
  * @module core/modules/TelemetryHandler
15
15
  */
16
16
  import type { NeuroLink } from "../../neurolink.js";
17
- import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, RawUsageObject, TextGenerationOptions, TokenUsage } from "../../types/index.js";
17
+ import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, RawUsageObject, TextGenerationOptions, TokenUsage, TTSMetadata } from "../../types/index.js";
18
18
  /**
19
19
  * TelemetryHandler class - Handles analytics and telemetry for AI providers
20
20
  */
@@ -35,6 +35,10 @@ export declare class TelemetryHandler {
35
35
  * Record performance metrics for a generation
36
36
  */
37
37
  recordPerformanceMetrics(usage: RawUsageObject | undefined, responseTime: number): Promise<void>;
38
+ /**
39
+ * Record a TTS synthesis failure without affecting the generation result.
40
+ */
41
+ recordTTSFailure(ttsProvider: string, error: NonNullable<TTSMetadata["error"]>, latency: number): void;
38
42
  /**
39
43
  * Calculate actual cost based on token usage and provider configuration.
40
44
  *
@@ -100,6 +100,26 @@ export class TelemetryHandler {
100
100
  logger.warn("⚠️ Performance recording failed:", perfError);
101
101
  }
102
102
  }
103
+ /**
104
+ * Record a TTS synthesis failure without affecting the generation result.
105
+ */
106
+ recordTTSFailure(ttsProvider, error, latency) {
107
+ try {
108
+ const labels = {
109
+ provider: ttsProvider,
110
+ error_code: error.code,
111
+ ...(error.retriable !== undefined
112
+ ? { retriable: error.retriable.toString() }
113
+ : {}),
114
+ };
115
+ const telemetry = TelemetryService.getInstance();
116
+ telemetry.recordCustomMetric("tts_failures", 1, labels);
117
+ telemetry.recordCustomHistogram("tts_failure_latency_ms", latency, labels);
118
+ }
119
+ catch (telemetryError) {
120
+ logger.warn("TTS failure telemetry recording failed:", telemetryError);
121
+ }
122
+ }
103
123
  /**
104
124
  * Calculate actual cost based on token usage and provider configuration.
105
125
  *
@@ -4,6 +4,7 @@
4
4
  * Provides fault tolerance and prevents server overload
5
5
  */
6
6
  import { mcpLogger } from "../utils/logger.js";
7
+ import { parseRetryAfterMs } from "../utils/retryAfter.js";
7
8
  import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../observability/index.js";
8
9
  import { getActiveTraceContext } from "../telemetry/traceContext.js";
9
10
  /**
@@ -176,48 +177,40 @@ export class HTTPRateLimiter {
176
177
  * @returns Wait time in milliseconds, or 0 if no rate limit headers found
177
178
  */
178
179
  handleRateLimitResponse(headers) {
179
- // Check for Retry-After header (standard HTTP 429 response)
180
+ const parsedWaitTimeMs = parseRetryAfterMs(headers);
181
+ // Keep the existing source-specific logs while delegating delay parsing.
180
182
  const retryAfter = headers.get("Retry-After");
181
- if (retryAfter) {
182
- // Retry-After can be either a number of seconds or an HTTP-date
183
+ if (retryAfter && parsedWaitTimeMs !== undefined) {
183
184
  const seconds = parseInt(retryAfter, 10);
184
185
  if (!isNaN(seconds)) {
185
- // It's a number of seconds
186
- const waitTimeMs = seconds * 1000;
187
186
  mcpLogger.info(`[HTTPRateLimiter] Server requested retry after ${seconds} seconds`);
188
- return waitTimeMs;
187
+ return parsedWaitTimeMs;
189
188
  }
190
189
  else {
191
- // Try to parse as HTTP-date
192
190
  const retryDate = new Date(retryAfter);
193
191
  if (!isNaN(retryDate.getTime())) {
194
- const waitTimeMs = Math.max(0, retryDate.getTime() - Date.now());
195
- mcpLogger.info(`[HTTPRateLimiter] Server requested retry at ${retryDate.toISOString()} (${waitTimeMs}ms)`);
196
- return waitTimeMs;
192
+ mcpLogger.info(`[HTTPRateLimiter] Server requested retry at ${retryDate.toISOString()} (${parsedWaitTimeMs}ms)`);
193
+ return parsedWaitTimeMs;
197
194
  }
198
195
  }
199
196
  }
200
197
  // Check for X-RateLimit-Reset header (common non-standard header)
201
198
  const rateLimitReset = headers.get("X-RateLimit-Reset");
202
- if (rateLimitReset) {
199
+ if (rateLimitReset && parsedWaitTimeMs !== undefined) {
203
200
  const resetTimestamp = parseInt(rateLimitReset, 10);
204
201
  if (!isNaN(resetTimestamp)) {
205
- // Could be Unix timestamp (seconds) or milliseconds
206
202
  const resetTime = resetTimestamp > 1e12 ? resetTimestamp : resetTimestamp * 1000;
207
- const waitTimeMs = Math.max(0, resetTime - Date.now());
208
- mcpLogger.info(`[HTTPRateLimiter] Rate limit resets at ${new Date(resetTime).toISOString()} (${waitTimeMs}ms)`);
209
- return waitTimeMs;
203
+ mcpLogger.info(`[HTTPRateLimiter] Rate limit resets at ${new Date(resetTime).toISOString()} (${parsedWaitTimeMs}ms)`);
204
+ return parsedWaitTimeMs;
210
205
  }
211
206
  }
212
207
  // Check for X-RateLimit-Remaining header
213
208
  const remaining = headers.get("X-RateLimit-Remaining");
214
- if (remaining === "0") {
215
- // No remaining requests, use default backoff
216
- const defaultBackoffMs = 1000;
217
- mcpLogger.info(`[HTTPRateLimiter] Rate limit exhausted, using default backoff: ${defaultBackoffMs}ms`);
218
- return defaultBackoffMs;
209
+ if (remaining === "0" && parsedWaitTimeMs !== undefined) {
210
+ mcpLogger.info(`[HTTPRateLimiter] Rate limit exhausted, using default backoff: ${parsedWaitTimeMs}ms`);
211
+ return parsedWaitTimeMs;
219
212
  }
220
- return 0;
213
+ return parsedWaitTimeMs ?? 0;
221
214
  }
222
215
  /**
223
216
  * Get the number of remaining tokens
@@ -1418,9 +1418,25 @@ export type TextGenerationResult = {
1418
1418
  /**
1419
1419
  * Enhanced result type with optional analytics/evaluation
1420
1420
  */
1421
+ export type TTSMetadata = {
1422
+ /** Whether TTS synthesis was invoked. False indicates TTS was skipped. */
1423
+ attempted: boolean;
1424
+ /** Whether TTS synthesis completed successfully. */
1425
+ success: boolean;
1426
+ /** Structured synthesis error details, present only when synthesis failed. */
1427
+ error?: {
1428
+ code: string;
1429
+ message: string;
1430
+ retriable?: boolean;
1431
+ };
1432
+ /** TTS synthesis time in milliseconds. */
1433
+ latency?: number;
1434
+ };
1421
1435
  export type EnhancedGenerateResult = GenerateResult & {
1422
1436
  analytics?: AnalyticsData;
1423
1437
  evaluation?: EvaluationData;
1438
+ /** Outcome metadata when TTS was enabled for this generation. */
1439
+ ttsMetadata?: TTSMetadata;
1424
1440
  };
1425
1441
  /**
1426
1442
  * NL-004: Model alias/deprecation configuration.
@@ -70,6 +70,7 @@ export declare class NeuroLinkError extends Error {
70
70
  readonly category: ErrorCategory;
71
71
  readonly severity: ErrorSeverity;
72
72
  readonly retriable: boolean;
73
+ readonly retryAfterMs?: number;
73
74
  readonly context: Record<string, unknown>;
74
75
  readonly timestamp: Date;
75
76
  readonly toolName?: string;
@@ -80,6 +81,7 @@ export declare class NeuroLinkError extends Error {
80
81
  category: ErrorCategory;
81
82
  severity: ErrorSeverity;
82
83
  retriable: boolean;
84
+ retryAfterMs?: number;
83
85
  context?: Record<string, unknown>;
84
86
  originalError?: Error;
85
87
  toolName?: string;
@@ -87,6 +87,7 @@ export class NeuroLinkError extends Error {
87
87
  category;
88
88
  severity;
89
89
  retriable;
90
+ retryAfterMs;
90
91
  context;
91
92
  timestamp;
92
93
  toolName;
@@ -98,6 +99,7 @@ export class NeuroLinkError extends Error {
98
99
  this.category = options.category;
99
100
  this.severity = options.severity;
100
101
  this.retriable = options.retriable;
102
+ this.retryAfterMs = options.retryAfterMs;
101
103
  this.context = options.context || {};
102
104
  this.timestamp = new Date();
103
105
  this.toolName = options.toolName;