@juspay/neurolink 10.7.0 → 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.
- package/CHANGELOG.md +17 -0
- package/dist/adapters/replicate/predictionLifecycle.d.ts +1 -1
- package/dist/adapters/replicate/predictionLifecycle.js +50 -45
- package/dist/agent/directTools.d.ts +2 -2
- package/dist/browser/neurolink.min.js +373 -373
- package/dist/cli/commands/proxyAnalyze.js +1 -1
- package/dist/core/baseProvider.d.ts +1 -0
- package/dist/core/baseProvider.js +74 -11
- package/dist/core/modules/TelemetryHandler.d.ts +5 -1
- package/dist/core/modules/TelemetryHandler.js +20 -0
- package/dist/lib/adapters/replicate/predictionLifecycle.d.ts +1 -1
- package/dist/lib/adapters/replicate/predictionLifecycle.js +50 -45
- package/dist/lib/core/baseProvider.d.ts +1 -0
- package/dist/lib/core/baseProvider.js +74 -11
- package/dist/lib/core/modules/TelemetryHandler.d.ts +5 -1
- package/dist/lib/core/modules/TelemetryHandler.js +20 -0
- package/dist/lib/mcp/httpRateLimiter.js +14 -21
- package/dist/lib/proxy/proxyAnalysis.js +27 -9
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +16 -2
- package/dist/lib/server/routes/claudeProxyRoutes.js +15 -9
- package/dist/lib/types/generate.d.ts +16 -0
- package/dist/lib/types/proxy.d.ts +3 -0
- package/dist/lib/utils/errorHandling.d.ts +2 -0
- package/dist/lib/utils/errorHandling.js +2 -0
- package/dist/lib/utils/pdfProcessor.js +2 -2
- package/dist/lib/utils/providerRetry.d.ts +5 -1
- package/dist/lib/utils/providerRetry.js +34 -8
- package/dist/lib/utils/retryAfter.d.ts +7 -0
- package/dist/lib/utils/retryAfter.js +44 -0
- package/dist/mcp/httpRateLimiter.js +14 -21
- package/dist/proxy/proxyAnalysis.js +27 -9
- package/dist/server/routes/claudeProxyRoutes.d.ts +16 -2
- package/dist/server/routes/claudeProxyRoutes.js +15 -9
- package/dist/types/generate.d.ts +16 -0
- package/dist/types/proxy.d.ts +3 -0
- package/dist/utils/errorHandling.d.ts +2 -0
- package/dist/utils/errorHandling.js +2 -0
- package/dist/utils/pdfProcessor.js +2 -2
- package/dist/utils/providerRetry.d.ts +5 -1
- package/dist/utils/providerRetry.js +34 -8
- package/dist/utils/retryAfter.d.ts +7 -0
- package/dist/utils/retryAfter.js +43 -0
- package/package.json +3 -2
|
@@ -44,7 +44,7 @@ function printAnalysis(report) {
|
|
|
44
44
|
logger.always("");
|
|
45
45
|
logger.always(chalk.bold(" Account Routing"));
|
|
46
46
|
if (report.coverage.routingDecisions) {
|
|
47
|
-
logger.always(` Decisions: ${report.routing.records.length}, modes: ${JSON.stringify(report.routing.modes)}`);
|
|
47
|
+
logger.always(` Decisions: ${report.routing.totalRecords} (${report.routing.records.length} retained), modes: ${JSON.stringify(report.routing.modes)}`);
|
|
48
48
|
logger.always(` Selection reasons: ${JSON.stringify(report.routing.selectionReasons)}`);
|
|
49
49
|
logger.always(` Initial accounts: ${JSON.stringify(report.routing.initialAccounts)}`);
|
|
50
50
|
logger.always(` Final account changed after retry: ${report.routing.finalAccountChanges}, outside candidate set: ${report.routing.finalOutsideCandidateSet}`);
|
|
@@ -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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|