UNPKG

@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

113 lines (112 loc) 4.94 kB
/** * 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 { 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`); }