@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
114 lines • 5.01 kB
JavaScript
/**
* Provider-level retry utility for AI SDK calls (NL11)
*
* The Vercel AI SDK's `generateText()` and `streamText()` have built-in retry
* logic (`_retryWithExponentialBackoff()` with default `maxRetries: 2`) that
* retries on HTTP 429/500/503. These retries are completely invisible to OTel
* because they happen inside the AI SDK.
*
* This module provides an instrumented retry wrapper that:
* 1. Disables the AI SDK's internal retries (via `maxRetries: 0`)
* 2. Implements our own retry loop with full OTel span events
* 3. Records retry attempts, delays, status codes, and total attempt count
*
* @module utils/providerRetry
*/
import {} from "@opentelemetry/api";
import { APICallError } from "@ai-sdk/provider";
import { logger } from "./logger.js";
/** Maximum number of retry attempts after the initial call (total = 1 + MAX_PROVIDER_RETRIES). */
export const MAX_PROVIDER_RETRIES = 2;
/** Base delay in ms for exponential backoff between retries. */
export const BASE_RETRY_DELAY_MS = 1000;
/**
* Check whether an error thrown by the AI SDK is retryable.
*
* Uses `APICallError.isInstance()` for proper type-safe detection (the class
* uses a branded symbol marker, so `instanceof` doesn't work across package
* boundaries). Falls back to duck-typing for non-APICallError cases.
*/
export function isRetryableProviderError(error) {
// Preferred path: use the AI SDK's own branded type check + isRetryable flag
if (APICallError.isInstance(error)) {
return error.isRetryable;
}
// Fallback: duck-type for status codes on errors that aren't APICallError
if (error && typeof error === "object" && "statusCode" in error) {
const statusCode = error.statusCode;
return statusCode === 429 || statusCode >= 500;
}
return false;
}
/**
* Extract the HTTP status code from an AI SDK error, if available.
*/
export function getErrorStatusCode(error) {
if (APICallError.isInstance(error)) {
return error.statusCode;
}
if (error && typeof error === "object" && "statusCode" in error) {
return error.statusCode;
}
return undefined;
}
/**
* Execute a provider call with instrumented retry logic.
*
* @param operation - The async operation to execute (should already use `maxRetries: 0`)
* @param span - The OTel span to annotate with retry events and attributes
* @param label - A human-readable label for log messages (e.g. "generateText", "streamText")
* @returns The result of the operation
*/
export async function withProviderRetry(operation, span, label) {
for (let attempt = 0; attempt <= MAX_PROVIDER_RETRIES; attempt++) {
try {
const result = await operation();
// Record how many attempts it took on the span
span.setAttribute("gen_ai.provider.total_attempts", attempt + 1);
if (attempt > 0) {
logger.info(`[providerRetry] ${label} succeeded after ${attempt + 1} attempts`);
}
return result;
}
catch (error) {
const retryable = isRetryableProviderError(error);
const statusCode = getErrorStatusCode(error);
const errorMessage = error instanceof Error ? error.message : String(error);
if (!retryable || attempt === MAX_PROVIDER_RETRIES) {
// Record failure details before re-throwing
span.setAttribute("gen_ai.provider.total_attempts", attempt + 1);
if (attempt > 0) {
span.setAttribute("gen_ai.provider.retries_exhausted", true);
}
logger.warn(`[providerRetry] ${label} failed (non-retryable or retries exhausted)`, {
attempt: attempt + 1,
retryable,
statusCode,
error: errorMessage,
});
throw error;
}
// Calculate exponential backoff delay
const delay = BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
// Record retry event on the OTel span
span.addEvent("gen_ai.provider.retry", {
"retry.attempt": attempt + 1,
"retry.delay_ms": delay,
...(statusCode !== undefined && { "retry.status_code": statusCode }),
"retry.error": errorMessage.slice(0, 256),
});
logger.warn(`[providerRetry] ${label} retrying after ${statusCode || "unknown"} error`, {
attempt: attempt + 1,
maxRetries: MAX_PROVIDER_RETRIES,
delayMs: delay,
statusCode,
error: errorMessage,
});
await new Promise((r) => setTimeout(r, delay));
}
}
// This should never be reached due to the throw inside the loop,
// but TypeScript requires it for exhaustiveness.
throw new Error(`[providerRetry] ${label} exhausted all retries`);
}
//# sourceMappingURL=providerRetry.js.map