"use strict"; /** * Suno API HTTP client with rate limiting, proxy support, retries and polling helpers. * * Rate limit: 20 requests per 10 seconds (sunoapi.org). * Proxy support via SUNO_HTTP_PROXY / HTTPS_PROXY / HTTP_PROXY env vars. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SunoClient = void 0; const undici_1 = require("undici"); function getProxyUrl() { return (process.env.SUNO_HTTP_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy); } function isRetryableError(error) { if (error instanceof Error) { const msg = error.message.toLowerCase(); return msg.includes('timeout') || msg.includes('econnreset') || msg.includes('socket') || msg.includes('fetch failed'); } return false; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } class SunoClient { baseUrl = 'https://api.sunoapi.org/api/v1'; apiKey; maxRequests = 20; windowMs = 10_000; proxyAgent; requestTimeoutMs = 30_000; maxRetries = 2; requestTimestamps = []; constructor(apiKey) { this.apiKey = apiKey; const proxyUrl = getProxyUrl(); if (proxyUrl) { this.proxyAgent = new undici_1.ProxyAgent(proxyUrl); } } /** * Enforce rate limit: max 20 requests per 10 seconds. */ async throttle() { while (true) { const now = Date.now(); this.requestTimestamps = this.requestTimestamps.filter((ts) => now - ts < this.windowMs); if (this.requestTimestamps.length < this.maxRequests) { this.requestTimestamps.push(now); return; } const oldest = this.requestTimestamps[0]; const wait = Math.max(0, this.windowMs - (now - oldest)); await sleep(wait); } } async request(method, path, body) { await this.throttle(); const url = path.startsWith('http') ? path : `${this.baseUrl}${path}`; const headers = { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }; let lastError; for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { const fetchOptions = { method, headers, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(this.requestTimeoutMs), }; if (this.proxyAgent) { fetchOptions.dispatcher = this.proxyAgent; } const response = await (0, undici_1.fetch)(url, fetchOptions); const text = await response.text(); let json; try { json = JSON.parse(text); } catch { throw new Error(`Invalid JSON response from Suno API: ${text}`); } // Retry on transient server errors / rate-limit before throwing. if (json.code === 429 || json.code === 503 || json.code === 504) { if (attempt < this.maxRetries) { const backoff = Math.min(1000 * 2 ** attempt, 8000); await sleep(backoff); continue; } } if (json.code !== 200) { throw new Error(`Suno API error ${json.code}: ${json.msg || 'unknown error'}`); } return json; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); const retryable = isRetryableError(lastError); if (retryable && attempt < this.maxRetries) { const backoff = Math.min(1000 * 2 ** attempt, 8000); await sleep(backoff); continue; } throw lastError; } } throw lastError ?? new Error('Suno API request failed'); } get(path) { return this.request('GET', path); } post(path, body) { return this.request('POST', path, body); } /** * Poll task status until terminal state or timeout. */ async pollTaskStatus(taskId, options = {}) { const intervalMs = options.intervalMs ?? 15_000; const maxAttempts = options.maxAttempts ?? 60; const statusPath = options.statusPath ?? `/generate/record-info?taskId=${encodeURIComponent(taskId)}`; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const result = await this.get(statusPath); const status = result.data.status; if (status === 'SUCCESS' || status === 'FAILED' || status === 'CREATE_TASK_FAILED' || status === 'GENERATE_AUDIO_FAILED' || status === 'GENERATE_LYRICS_FAILED' || status === 'CALLBACK_EXCEPTION' || status === 'SENSITIVE_WORD_ERROR') { return result.data; } if (attempt < maxAttempts) { await sleep(intervalMs); } } throw new Error(`Polling timeout for task ${taskId} after ${maxAttempts} attempts`); } } exports.SunoClient = SunoClient; //# sourceMappingURL=client.js.map