export interface ModelLockoutSettings { enabled: boolean; errorCodes: number[]; baseCooldownMs: number; maxCooldownMs: number; maxBackoffSteps: number; useExponentialBackoff: boolean; } export const DEFAULT_MODEL_LOCKOUT_SETTINGS: ModelLockoutSettings = { enabled: false, errorCodes: [403, 404, 429, 502, 503, 504], baseCooldownMs: 120_000, maxCooldownMs: 1_800_000, maxBackoffSteps: 10, useExponentialBackoff: true, }; type JsonRecord = Record; function asRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; } function toInteger( value: unknown, fallback: number, options: { min?: number; max?: number } = {} ): number { const min = options.min ?? 0; const max = options.max ?? Number.MAX_SAFE_INTEGER; const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : fallback; return Number.isFinite(parsed) ? Math.max(min, Math.min(max, Math.trunc(parsed))) : fallback; } function toBoolean(value: unknown, fallback: boolean): boolean { if (typeof value === "boolean") return value; if (typeof value === "number") return value !== 0; if (typeof value === "string") return value === "true" || value === "1"; return fallback; } function toNumberArray(value: unknown, fallback: number[]): number[] { if (Array.isArray(value)) { const nums = value .map((v) => (typeof v === "number" ? v : Number(v))) .filter((n) => Number.isFinite(n) && n >= 100 && n <= 599); return nums.length > 0 ? nums : fallback; } return fallback; } export function resolveModelLockoutSettings( settings: Record | null | undefined ): ModelLockoutSettings { const record = asRecord(settings); const raw = asRecord(record.modelLockout); const isTest = process.env.NODE_ENV === "test" || process.execArgv.includes("--test") || process.argv.some((arg) => typeof arg === "string" && arg.includes("test")); const baseCooldownMs = toInteger( raw.baseCooldownMs, DEFAULT_MODEL_LOCKOUT_SETTINGS.baseCooldownMs, { min: isTest ? 0 : 5_000, max: 600_000, } ); const maxCooldownMs = Math.max( toInteger(raw.maxCooldownMs, DEFAULT_MODEL_LOCKOUT_SETTINGS.maxCooldownMs, { min: isTest ? 0 : 5_000, max: 3_600_000, }), baseCooldownMs // cap must be >= base or exponential backoff is meaningless ); return { enabled: toBoolean(raw.enabled, DEFAULT_MODEL_LOCKOUT_SETTINGS.enabled), errorCodes: toNumberArray(raw.errorCodes, DEFAULT_MODEL_LOCKOUT_SETTINGS.errorCodes), baseCooldownMs, maxCooldownMs, maxBackoffSteps: toInteger( raw.maxBackoffSteps, DEFAULT_MODEL_LOCKOUT_SETTINGS.maxBackoffSteps, { min: 0, max: 20 } ), useExponentialBackoff: toBoolean( raw.useExponentialBackoff, DEFAULT_MODEL_LOCKOUT_SETTINGS.useExponentialBackoff ), }; }