@juspay/neurolink 10.7.1 → 10.8.1
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 +6 -0
- 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 +22 -4
- package/dist/lib/types/cli.d.ts +1 -0
- package/dist/lib/types/generate.d.ts +16 -0
- package/dist/lib/types/proxy.d.ts +2 -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 +22 -4
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/generate.d.ts +16 -0
- package/dist/types/proxy.d.ts +2 -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
|
@@ -212,6 +212,19 @@ function parseSince(value, nowMs) {
|
|
|
212
212
|
}
|
|
213
213
|
return parsed;
|
|
214
214
|
}
|
|
215
|
+
function parseUntil(value, nowMs) {
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
parsed = parseSince(value, nowMs);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
throw new Error(`Invalid --until value "${value}". Use an ISO timestamp or a duration such as 6h, 1d, or 1w.`);
|
|
222
|
+
}
|
|
223
|
+
if (parsed > nowMs) {
|
|
224
|
+
throw new Error(`Invalid --until value "${value}". It must not be later than the analysis start time.`);
|
|
225
|
+
}
|
|
226
|
+
return parsed;
|
|
227
|
+
}
|
|
215
228
|
async function readJsonLines(filePath, onRecord, onMalformed) {
|
|
216
229
|
let linesRead = 0;
|
|
217
230
|
const lines = createInterface({
|
|
@@ -412,6 +425,10 @@ async function discoverLogFiles(logsDir) {
|
|
|
412
425
|
export async function analyzeProxyLogs(options) {
|
|
413
426
|
const nowMs = options?.nowMs ?? Date.now();
|
|
414
427
|
const sinceMs = parseSince(options?.since ?? "24h", nowMs);
|
|
428
|
+
const untilMs = options?.until ? parseUntil(options.until, nowMs) : nowMs;
|
|
429
|
+
if (untilMs < sinceMs) {
|
|
430
|
+
throw new Error(`Invalid analysis window: --until must not be earlier than --since.`);
|
|
431
|
+
}
|
|
415
432
|
const logsDir = resolve(options?.logsDir ?? join(homedir(), ".neurolink", "logs"));
|
|
416
433
|
const { lifecycleFiles, requestFiles, attemptFiles, debugFiles } = await discoverLogFiles(logsDir);
|
|
417
434
|
const observedRanges = {
|
|
@@ -448,7 +465,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
448
465
|
for (const filePath of lifecycleFiles) {
|
|
449
466
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
450
467
|
const timestamp = observeTimestamp("lifecycle", record);
|
|
451
|
-
if (timestamp === null || timestamp < sinceMs) {
|
|
468
|
+
if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
|
|
452
469
|
return;
|
|
453
470
|
}
|
|
454
471
|
const event = stringValue(record.event);
|
|
@@ -530,7 +547,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
530
547
|
for (const filePath of attemptFiles) {
|
|
531
548
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
532
549
|
const timestamp = observeTimestamp("attempts", record);
|
|
533
|
-
if (timestamp === null || timestamp < sinceMs) {
|
|
550
|
+
if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
|
|
534
551
|
return;
|
|
535
552
|
}
|
|
536
553
|
const requestId = stringValue(record.requestId);
|
|
@@ -595,7 +612,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
595
612
|
for (const filePath of requestFiles) {
|
|
596
613
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
597
614
|
const timestamp = observeTimestamp("requests", record);
|
|
598
|
-
if (timestamp === null || timestamp < sinceMs) {
|
|
615
|
+
if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
|
|
599
616
|
return;
|
|
600
617
|
}
|
|
601
618
|
const requestId = stringValue(record.requestId);
|
|
@@ -649,7 +666,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
649
666
|
for (const filePath of debugFiles) {
|
|
650
667
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
651
668
|
const timestamp = observeTimestamp("debug", record);
|
|
652
|
-
if (timestamp === null || timestamp < sinceMs) {
|
|
669
|
+
if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
|
|
653
670
|
return;
|
|
654
671
|
}
|
|
655
672
|
if (record.type !== "body_capture") {
|
|
@@ -709,6 +726,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
709
726
|
return {
|
|
710
727
|
generatedAt: new Date(nowMs).toISOString(),
|
|
711
728
|
since: new Date(sinceMs).toISOString(),
|
|
729
|
+
until: new Date(untilMs).toISOString(),
|
|
712
730
|
logsDir,
|
|
713
731
|
files: {
|
|
714
732
|
lifecycle: lifecycleFiles.length,
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -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.
|
|
@@ -1247,6 +1247,7 @@ export type ProxyAnalysisStreamName = "lifecycle" | "requests" | "attempts" | "d
|
|
|
1247
1247
|
export type ProxyAnalysisReport = {
|
|
1248
1248
|
generatedAt: string;
|
|
1249
1249
|
since: string;
|
|
1250
|
+
until: string;
|
|
1250
1251
|
logsDir: string;
|
|
1251
1252
|
files: {
|
|
1252
1253
|
lifecycle: number;
|
|
@@ -1352,6 +1353,7 @@ export type ProxyAnalysisReport = {
|
|
|
1352
1353
|
export type ProxyAnalysisOptions = {
|
|
1353
1354
|
logsDir?: string;
|
|
1354
1355
|
since?: string;
|
|
1356
|
+
until?: string;
|
|
1355
1357
|
nowMs?: number;
|
|
1356
1358
|
};
|
|
1357
1359
|
/** Attempt timing retained while joining offline proxy log records. */
|
|
@@ -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;
|
|
@@ -91,14 +91,14 @@ const PDF_PROVIDER_CONFIGS = {
|
|
|
91
91
|
litellm: {
|
|
92
92
|
maxSizeMB: 10,
|
|
93
93
|
maxPages: 100,
|
|
94
|
-
supportsNative:
|
|
94
|
+
supportsNative: false, // LiteLLM is a proxy — underlying model may not support native PDF; default to safe text extraction
|
|
95
95
|
requiresCitations: false,
|
|
96
96
|
apiType: "files-api",
|
|
97
97
|
},
|
|
98
98
|
"openai-compatible": {
|
|
99
99
|
maxSizeMB: 10,
|
|
100
100
|
maxPages: 100,
|
|
101
|
-
supportsNative: false,
|
|
101
|
+
supportsNative: false,
|
|
102
102
|
requiresCitations: false,
|
|
103
103
|
apiType: "files-api",
|
|
104
104
|
},
|
|
@@ -18,6 +18,10 @@ import { type Span } from "@opentelemetry/api";
|
|
|
18
18
|
export declare const MAX_PROVIDER_RETRIES = 2;
|
|
19
19
|
/** Base delay in ms for exponential backoff between retries. */
|
|
20
20
|
export declare const BASE_RETRY_DELAY_MS = 1000;
|
|
21
|
+
/** Minimum delay in ms when a retryable response provides no retry timing. */
|
|
22
|
+
export declare const NO_HINT_FLOOR_MS = 10000;
|
|
23
|
+
/** Maximum server-requested retry delay honored by provider retries. */
|
|
24
|
+
export declare const MAX_RETRY_AFTER_MS = 120000;
|
|
21
25
|
/**
|
|
22
26
|
* Check whether an error thrown by the AI SDK is retryable.
|
|
23
27
|
*
|
|
@@ -38,4 +42,4 @@ export declare function getErrorStatusCode(error: unknown): number | undefined;
|
|
|
38
42
|
* @param label - A human-readable label for log messages (e.g. "generateText", "streamText")
|
|
39
43
|
* @returns The result of the operation
|
|
40
44
|
*/
|
|
41
|
-
export declare function withProviderRetry<T>(operation: () => Promise<T>, span: Span, label: string): Promise<T>;
|
|
45
|
+
export declare function withProviderRetry<T>(operation: () => Promise<T>, span: Span | undefined, label: string, sleep?: (delayMs: number) => Promise<void>): Promise<T>;
|
|
@@ -14,12 +14,19 @@
|
|
|
14
14
|
* @module utils/providerRetry
|
|
15
15
|
*/
|
|
16
16
|
import {} from "@opentelemetry/api";
|
|
17
|
+
import { NeuroLinkError } from "./errorHandling.js";
|
|
17
18
|
import { logger } from "./logger.js";
|
|
18
19
|
import { APICallError } from "./generationErrors.js";
|
|
20
|
+
import { parseRetryAfterMs } from "./retryAfter.js";
|
|
19
21
|
/** Maximum number of retry attempts after the initial call (total = 1 + MAX_PROVIDER_RETRIES). */
|
|
20
22
|
export const MAX_PROVIDER_RETRIES = 2;
|
|
21
23
|
/** Base delay in ms for exponential backoff between retries. */
|
|
22
24
|
export const BASE_RETRY_DELAY_MS = 1000;
|
|
25
|
+
/** Minimum delay in ms when a retryable response provides no retry timing. */
|
|
26
|
+
export const NO_HINT_FLOOR_MS = 10_000;
|
|
27
|
+
/** Maximum server-requested retry delay honored by provider retries. */
|
|
28
|
+
export const MAX_RETRY_AFTER_MS = 120_000;
|
|
29
|
+
const sleepWithTimeout = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
23
30
|
/**
|
|
24
31
|
* Check whether an error thrown by the AI SDK is retryable.
|
|
25
32
|
*
|
|
@@ -32,6 +39,9 @@ export function isRetryableProviderError(error) {
|
|
|
32
39
|
if (APICallError.isInstance(error)) {
|
|
33
40
|
return error.isRetryable;
|
|
34
41
|
}
|
|
42
|
+
if (error instanceof NeuroLinkError) {
|
|
43
|
+
return error.retriable;
|
|
44
|
+
}
|
|
35
45
|
// Fallback: duck-type for status codes on errors that aren't APICallError
|
|
36
46
|
if (error && typeof error === "object" && "statusCode" in error) {
|
|
37
47
|
const statusCode = error.statusCode;
|
|
@@ -51,6 +61,18 @@ export function getErrorStatusCode(error) {
|
|
|
51
61
|
}
|
|
52
62
|
return undefined;
|
|
53
63
|
}
|
|
64
|
+
function getRetryAfterMs(error) {
|
|
65
|
+
if (APICallError.isInstance(error) && error.responseHeaders) {
|
|
66
|
+
const parsedDelay = parseRetryAfterMs(error.responseHeaders);
|
|
67
|
+
if (parsedDelay !== undefined) {
|
|
68
|
+
return parsedDelay;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (error instanceof NeuroLinkError && error.retryAfterMs !== undefined) {
|
|
72
|
+
return error.retryAfterMs;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
54
76
|
/**
|
|
55
77
|
* Execute a provider call with instrumented retry logic.
|
|
56
78
|
*
|
|
@@ -59,12 +81,12 @@ export function getErrorStatusCode(error) {
|
|
|
59
81
|
* @param label - A human-readable label for log messages (e.g. "generateText", "streamText")
|
|
60
82
|
* @returns The result of the operation
|
|
61
83
|
*/
|
|
62
|
-
export async function withProviderRetry(operation, span, label) {
|
|
84
|
+
export async function withProviderRetry(operation, span, label, sleep = sleepWithTimeout) {
|
|
63
85
|
for (let attempt = 0; attempt <= MAX_PROVIDER_RETRIES; attempt++) {
|
|
64
86
|
try {
|
|
65
87
|
const result = await operation();
|
|
66
88
|
// Record how many attempts it took on the span
|
|
67
|
-
span
|
|
89
|
+
span?.setAttribute("gen_ai.provider.total_attempts", attempt + 1);
|
|
68
90
|
if (attempt > 0) {
|
|
69
91
|
logger.info(`[providerRetry] ${label} succeeded after ${attempt + 1} attempts`);
|
|
70
92
|
}
|
|
@@ -76,9 +98,9 @@ export async function withProviderRetry(operation, span, label) {
|
|
|
76
98
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
77
99
|
if (!retryable || attempt === MAX_PROVIDER_RETRIES) {
|
|
78
100
|
// Record failure details before re-throwing
|
|
79
|
-
span
|
|
101
|
+
span?.setAttribute("gen_ai.provider.total_attempts", attempt + 1);
|
|
80
102
|
if (attempt > 0) {
|
|
81
|
-
span
|
|
103
|
+
span?.setAttribute("gen_ai.provider.retries_exhausted", true);
|
|
82
104
|
}
|
|
83
105
|
logger.warn(`[providerRetry] ${label} failed (non-retryable or retries exhausted)`, {
|
|
84
106
|
attempt: attempt + 1,
|
|
@@ -88,10 +110,14 @@ export async function withProviderRetry(operation, span, label) {
|
|
|
88
110
|
});
|
|
89
111
|
throw error;
|
|
90
112
|
}
|
|
91
|
-
|
|
92
|
-
const
|
|
113
|
+
const retryAfterMs = getRetryAfterMs(error);
|
|
114
|
+
const boundedRetryAfterMs = retryAfterMs === undefined
|
|
115
|
+
? undefined
|
|
116
|
+
: Math.min(MAX_RETRY_AFTER_MS, Math.max(0, retryAfterMs));
|
|
117
|
+
const delay = boundedRetryAfterMs ??
|
|
118
|
+
Math.max(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), NO_HINT_FLOOR_MS);
|
|
93
119
|
// Record retry event on the OTel span
|
|
94
|
-
span
|
|
120
|
+
span?.addEvent("gen_ai.provider.retry", {
|
|
95
121
|
"retry.attempt": attempt + 1,
|
|
96
122
|
"retry.delay_ms": delay,
|
|
97
123
|
...(statusCode !== undefined && { "retry.status_code": statusCode }),
|
|
@@ -104,7 +130,7 @@ export async function withProviderRetry(operation, span, label) {
|
|
|
104
130
|
statusCode,
|
|
105
131
|
error: errorMessage,
|
|
106
132
|
});
|
|
107
|
-
await
|
|
133
|
+
await sleep(delay);
|
|
108
134
|
}
|
|
109
135
|
}
|
|
110
136
|
// This should never be reached due to the throw inside the loop,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse standard and commonly used rate-limit response headers.
|
|
3
|
+
*
|
|
4
|
+
* @returns The requested delay in milliseconds, or undefined when the
|
|
5
|
+
* response does not provide a usable rate-limit delay.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseRetryAfterMs(headers: Pick<Headers, "get"> | Readonly<Record<string, string>>): number | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function getHeader(headers, headerName) {
|
|
2
|
+
if ("get" in headers && typeof headers.get === "function") {
|
|
3
|
+
return headers.get(headerName);
|
|
4
|
+
}
|
|
5
|
+
const normalizedHeaderName = headerName.toLowerCase();
|
|
6
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
7
|
+
if (name.toLowerCase() === normalizedHeaderName) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse standard and commonly used rate-limit response headers.
|
|
15
|
+
*
|
|
16
|
+
* @returns The requested delay in milliseconds, or undefined when the
|
|
17
|
+
* response does not provide a usable rate-limit delay.
|
|
18
|
+
*/
|
|
19
|
+
export function parseRetryAfterMs(headers) {
|
|
20
|
+
const retryAfter = getHeader(headers, "Retry-After");
|
|
21
|
+
if (retryAfter) {
|
|
22
|
+
const seconds = parseInt(retryAfter, 10);
|
|
23
|
+
if (!Number.isNaN(seconds)) {
|
|
24
|
+
return Math.max(0, seconds * 1000);
|
|
25
|
+
}
|
|
26
|
+
const retryDate = new Date(retryAfter);
|
|
27
|
+
if (!Number.isNaN(retryDate.getTime())) {
|
|
28
|
+
return Math.max(0, retryDate.getTime() - Date.now());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const rateLimitReset = getHeader(headers, "X-RateLimit-Reset");
|
|
32
|
+
if (rateLimitReset) {
|
|
33
|
+
const resetTimestamp = parseInt(rateLimitReset, 10);
|
|
34
|
+
if (!Number.isNaN(resetTimestamp)) {
|
|
35
|
+
const resetTime = resetTimestamp > 1e12 ? resetTimestamp : resetTimestamp * 1000;
|
|
36
|
+
return Math.max(0, resetTime - Date.now());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (getHeader(headers, "X-RateLimit-Remaining") === "0") {
|
|
40
|
+
return 1000;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=retryAfter.js.map
|
|
@@ -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
|
|
@@ -212,6 +212,19 @@ function parseSince(value, nowMs) {
|
|
|
212
212
|
}
|
|
213
213
|
return parsed;
|
|
214
214
|
}
|
|
215
|
+
function parseUntil(value, nowMs) {
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
parsed = parseSince(value, nowMs);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
throw new Error(`Invalid --until value "${value}". Use an ISO timestamp or a duration such as 6h, 1d, or 1w.`);
|
|
222
|
+
}
|
|
223
|
+
if (parsed > nowMs) {
|
|
224
|
+
throw new Error(`Invalid --until value "${value}". It must not be later than the analysis start time.`);
|
|
225
|
+
}
|
|
226
|
+
return parsed;
|
|
227
|
+
}
|
|
215
228
|
async function readJsonLines(filePath, onRecord, onMalformed) {
|
|
216
229
|
let linesRead = 0;
|
|
217
230
|
const lines = createInterface({
|
|
@@ -412,6 +425,10 @@ async function discoverLogFiles(logsDir) {
|
|
|
412
425
|
export async function analyzeProxyLogs(options) {
|
|
413
426
|
const nowMs = options?.nowMs ?? Date.now();
|
|
414
427
|
const sinceMs = parseSince(options?.since ?? "24h", nowMs);
|
|
428
|
+
const untilMs = options?.until ? parseUntil(options.until, nowMs) : nowMs;
|
|
429
|
+
if (untilMs < sinceMs) {
|
|
430
|
+
throw new Error(`Invalid analysis window: --until must not be earlier than --since.`);
|
|
431
|
+
}
|
|
415
432
|
const logsDir = resolve(options?.logsDir ?? join(homedir(), ".neurolink", "logs"));
|
|
416
433
|
const { lifecycleFiles, requestFiles, attemptFiles, debugFiles } = await discoverLogFiles(logsDir);
|
|
417
434
|
const observedRanges = {
|
|
@@ -448,7 +465,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
448
465
|
for (const filePath of lifecycleFiles) {
|
|
449
466
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
450
467
|
const timestamp = observeTimestamp("lifecycle", record);
|
|
451
|
-
if (timestamp === null || timestamp < sinceMs) {
|
|
468
|
+
if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
|
|
452
469
|
return;
|
|
453
470
|
}
|
|
454
471
|
const event = stringValue(record.event);
|
|
@@ -530,7 +547,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
530
547
|
for (const filePath of attemptFiles) {
|
|
531
548
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
532
549
|
const timestamp = observeTimestamp("attempts", record);
|
|
533
|
-
if (timestamp === null || timestamp < sinceMs) {
|
|
550
|
+
if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
|
|
534
551
|
return;
|
|
535
552
|
}
|
|
536
553
|
const requestId = stringValue(record.requestId);
|
|
@@ -595,7 +612,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
595
612
|
for (const filePath of requestFiles) {
|
|
596
613
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
597
614
|
const timestamp = observeTimestamp("requests", record);
|
|
598
|
-
if (timestamp === null || timestamp < sinceMs) {
|
|
615
|
+
if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
|
|
599
616
|
return;
|
|
600
617
|
}
|
|
601
618
|
const requestId = stringValue(record.requestId);
|
|
@@ -649,7 +666,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
649
666
|
for (const filePath of debugFiles) {
|
|
650
667
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
651
668
|
const timestamp = observeTimestamp("debug", record);
|
|
652
|
-
if (timestamp === null || timestamp < sinceMs) {
|
|
669
|
+
if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
|
|
653
670
|
return;
|
|
654
671
|
}
|
|
655
672
|
if (record.type !== "body_capture") {
|
|
@@ -709,6 +726,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
709
726
|
return {
|
|
710
727
|
generatedAt: new Date(nowMs).toISOString(),
|
|
711
728
|
since: new Date(sinceMs).toISOString(),
|
|
729
|
+
until: new Date(untilMs).toISOString(),
|
|
712
730
|
logsDir,
|
|
713
731
|
files: {
|
|
714
732
|
lifecycle: lifecycleFiles.length,
|
package/dist/types/cli.d.ts
CHANGED
package/dist/types/generate.d.ts
CHANGED
|
@@ -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.
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -1247,6 +1247,7 @@ export type ProxyAnalysisStreamName = "lifecycle" | "requests" | "attempts" | "d
|
|
|
1247
1247
|
export type ProxyAnalysisReport = {
|
|
1248
1248
|
generatedAt: string;
|
|
1249
1249
|
since: string;
|
|
1250
|
+
until: string;
|
|
1250
1251
|
logsDir: string;
|
|
1251
1252
|
files: {
|
|
1252
1253
|
lifecycle: number;
|
|
@@ -1352,6 +1353,7 @@ export type ProxyAnalysisReport = {
|
|
|
1352
1353
|
export type ProxyAnalysisOptions = {
|
|
1353
1354
|
logsDir?: string;
|
|
1354
1355
|
since?: string;
|
|
1356
|
+
until?: string;
|
|
1355
1357
|
nowMs?: number;
|
|
1356
1358
|
};
|
|
1357
1359
|
/** Attempt timing retained while joining offline proxy log records. */
|
|
@@ -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;
|
|
@@ -91,14 +91,14 @@ const PDF_PROVIDER_CONFIGS = {
|
|
|
91
91
|
litellm: {
|
|
92
92
|
maxSizeMB: 10,
|
|
93
93
|
maxPages: 100,
|
|
94
|
-
supportsNative:
|
|
94
|
+
supportsNative: false, // LiteLLM is a proxy — underlying model may not support native PDF; default to safe text extraction
|
|
95
95
|
requiresCitations: false,
|
|
96
96
|
apiType: "files-api",
|
|
97
97
|
},
|
|
98
98
|
"openai-compatible": {
|
|
99
99
|
maxSizeMB: 10,
|
|
100
100
|
maxPages: 100,
|
|
101
|
-
supportsNative: false,
|
|
101
|
+
supportsNative: false,
|
|
102
102
|
requiresCitations: false,
|
|
103
103
|
apiType: "files-api",
|
|
104
104
|
},
|
|
@@ -18,6 +18,10 @@ import { type Span } from "@opentelemetry/api";
|
|
|
18
18
|
export declare const MAX_PROVIDER_RETRIES = 2;
|
|
19
19
|
/** Base delay in ms for exponential backoff between retries. */
|
|
20
20
|
export declare const BASE_RETRY_DELAY_MS = 1000;
|
|
21
|
+
/** Minimum delay in ms when a retryable response provides no retry timing. */
|
|
22
|
+
export declare const NO_HINT_FLOOR_MS = 10000;
|
|
23
|
+
/** Maximum server-requested retry delay honored by provider retries. */
|
|
24
|
+
export declare const MAX_RETRY_AFTER_MS = 120000;
|
|
21
25
|
/**
|
|
22
26
|
* Check whether an error thrown by the AI SDK is retryable.
|
|
23
27
|
*
|
|
@@ -38,4 +42,4 @@ export declare function getErrorStatusCode(error: unknown): number | undefined;
|
|
|
38
42
|
* @param label - A human-readable label for log messages (e.g. "generateText", "streamText")
|
|
39
43
|
* @returns The result of the operation
|
|
40
44
|
*/
|
|
41
|
-
export declare function withProviderRetry<T>(operation: () => Promise<T>, span: Span, label: string): Promise<T>;
|
|
45
|
+
export declare function withProviderRetry<T>(operation: () => Promise<T>, span: Span | undefined, label: string, sleep?: (delayMs: number) => Promise<void>): Promise<T>;
|