import { ConvexError } from "convex/values"; import { FreepikApiError } from "./freepik"; export type ErrorCategory = | "credits" | "policy" | "timeout" | "transient" | "provider" | "unknown"; interface ErrorData { code?: string; [key: string]: unknown; } export function getErrorCode(error: unknown): string | undefined { if (error instanceof ConvexError) { const data = error.data as ErrorData; return data?.code; } if (error instanceof FreepikApiError) { return error.code; } return undefined; } export function getErrorSource(error: unknown): string | undefined { if (error instanceof FreepikApiError) { return error.source; } if (error && typeof error === "object") { const source = (error as { source?: unknown }).source; return typeof source === "string" ? source : undefined; } return undefined; } export function getProviderStatus(error: unknown): number | null { if (error instanceof FreepikApiError) { return typeof error.status === "number" ? error.status : null; } if (error && typeof error === "object") { const status = (error as { status?: unknown }).status; if (typeof status === "number" && Number.isFinite(status)) { return status; } } return null; } export function errorMessage(error: unknown): string { if (error instanceof Error) return error.message; return String(error ?? "Generation failed"); } function parseOpenRouterStatus(message: string): number | null { const match = message.match(/OpenRouter API error\s+(\d+)/i); if (!match) return null; const parsed = Number(match[1]); return Number.isFinite(parsed) ? parsed : null; } export function categorizeError(error: unknown): { category: ErrorCategory; retryable: boolean; } { const code = getErrorCode(error); const source = getErrorSource(error); const message = errorMessage(error); const lower = message.toLowerCase(); const status = getProviderStatus(error) ?? parseOpenRouterStatus(message); if (source === "freepik") { if (code === "model_unavailable") { return { category: "provider", retryable: status === 503, }; } if (code === "timeout") { return { category: "timeout", retryable: true }; } if (code === "transient") { return { category: "transient", retryable: true }; } } if ( code === "CREDITS_TEST_DISABLED" || code === "CREDITS_INVALID_AMOUNT" || code === "CREDITS_BALANCE_NOT_FOUND" || code === "CREDITS_DAILY_CAP_REACHED" || code === "CREDITS_CONCURRENCY_LIMIT" ) { return { category: "credits", retryable: false }; } if ( code === "OPENROUTER_MODEL_REFUSAL" || lower.includes("content policy") || lower.includes("policy") || lower.includes("moderation") || lower.includes("safety") || lower.includes("refusal") || lower.includes("policy_violation") ) { return { category: "policy", retryable: false }; } if (status !== null) { if (status >= 500 || status === 408 || status === 429 || status === 499) { return { category: "provider", retryable: true }; } if (status >= 400 && status < 500) { return { category: "provider", retryable: false }; } } if ( lower.includes("timeout") || lower.includes("timed out") || lower.includes("deadline") || lower.includes("abort") || lower.includes("etimedout") ) { return { category: "timeout", retryable: true }; } if ( lower.includes("fetch failed") || lower.includes("network") || lower.includes("connection") || lower.includes("econnreset") || lower.includes("temporarily unavailable") || lower.includes("service unavailable") || lower.includes("rate limit") || lower.includes("overloaded") ) { return { category: "transient", retryable: true }; } return { category: "unknown", retryable: false }; } export function formatTerminalStatusMessage(error: unknown): string { const code = getErrorCode(error); if (code) { return code; } const message = errorMessage(error).trim() || "Generation failed"; const { category } = categorizeError(error); const prefixByCategory: Record, string> = { credits: "Credits", policy: "Policy", timeout: "Timeout", transient: "Netzwerk", provider: "Provider", }; if (category === "unknown") { return message; } const prefix = prefixByCategory[category]; if (message.toLowerCase().startsWith(prefix.toLowerCase())) { return message; } return `${prefix}: ${message}`; } export function getVideoPollDelayMs(attempt: number): number { if (attempt <= 5) { return 5000; } if (attempt <= 15) { return 10000; } return 20000; } export function isVideoPollTimedOut(args: { attempt: number; maxAttempts: number; elapsedMs: number; maxTotalMs: number; }): boolean { return args.attempt > args.maxAttempts || args.elapsedMs > args.maxTotalMs; }