UNPKG

wavespeed

Version:

WaveSpeed Client SDK for Wavespeed API

286 lines (285 loc) 11.3 kB
"use strict"; /** * Input parameters for image generation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.WaveSpeed = exports.Prediction = void 0; /** * Prediction model representing an image generation job */ class Prediction { constructor(data, client) { this.id = data.id; this.model = data.model; this.status = data.status; this.input = data.input; this.outputs = data.outputs || []; this.urls = data.urls; this.has_nsfw_contents = data.has_nsfw_contents || []; this.created_at = data.created_at; this.error = data.error; this.executionTime = data.executionTime; this.client = client; } /** * Wait for the prediction to complete */ async wait() { if (this.status === 'completed' || this.status === 'failed') { return this; } return new Promise((resolve, reject) => { const checkStatus = async () => { try { const updated = await this.reload(); if (updated.status === 'completed' || updated.status === 'failed') { resolve(updated); } else { setTimeout(checkStatus, this.client.pollInterval * 1000); } } catch (error) { reject(error); } }; checkStatus(); }); } /** * Reload the prediction status */ async reload() { const response = await this.client.fetchWithTimeout(`predictions/${this.id}/result`); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to reload prediction: ${response.status} ${errorText}`); } const data = await response.json(); const updatedPrediction = new Prediction(data.data, this.client); // Update this instance with new data Object.assign(this, updatedPrediction); return this; } } exports.Prediction = Prediction; /** * WaveSpeed client for generating images */ class WaveSpeed { /** * Create a new WaveSpeed client * * @param apiKey Your WaveSpeed API key (or set WAVESPEED_API_KEY environment variable) * @param options Additional client options */ constructor(apiKey, options = {}) { this.baseUrl = 'https://api.wavespeed.ai/api/v3/'; // Browser-friendly environment variable handling const getEnvVar = (name) => { // Try to get from process.env for Node.js environments if (typeof process !== 'undefined' && process.env && process.env[name]) { return process.env[name]; } return undefined; }; this.apiKey = apiKey || getEnvVar('WAVESPEED_API_KEY') || ''; if (!this.apiKey) { throw new Error('API key is required. Provide it as a parameter or set the WAVESPEED_API_KEY environment variable.'); } if (options.baseUrl) { this.baseUrl = options.baseUrl; } this.pollInterval = options.pollInterval || Number(getEnvVar('WAVESPEED_POLL_INTERVAL')) || 0.5; this.timeout = options.timeout || Number(getEnvVar('WAVESPEED_TIMEOUT')) || 120; } /** * Fetch with timeout support * * @param path API path * @param options Fetch options */ async fetchWithTimeout(path, options = {}) { const { timeout = this.timeout * 1000, ...fetchOptions } = options; // Ensure headers exist if (options.isUpload) { fetchOptions.headers = { 'Authorization': `Bearer ${this.apiKey}`, }; } else { fetchOptions.headers = { 'Authorization': `Bearer ${this.apiKey}`, 'content-type': 'application/json', ...(fetchOptions.headers || {}), }; } // Default retry options const maxRetries = options.maxRetries || 3; const initialBackoff = 1000; // 1 second let retryCount = 0; // Function to determine if a response should be retried const shouldRetry = (response) => { // Retry on rate limit (429) for all requests // For GET requests, also retry on server errors (5xx) const method = (fetchOptions.method || 'GET').toUpperCase(); return response.status === 429 || (method === 'GET' && response.status >= 500); }; while (true) { // Use AbortController for timeout (supported in modern browsers) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { // Construct the full URL by joining baseUrl and path const baseUrl = options.isUpload ? this.baseUrl.replace('v3', 'v2') : this.baseUrl; const url = new URL(path.startsWith('/') ? path.substring(1) : path, baseUrl).toString(); const response = await fetch(url, { ...fetchOptions, signal: controller.signal }); // If the response is successful or we've used all retries, return it if (response.ok || !shouldRetry(response) || retryCount >= maxRetries) { return response; } // Otherwise, increment retry count and wait before retrying retryCount++; const backoffTime = this._getBackoffTime(retryCount, initialBackoff); // Log retry information if console is available if (typeof console !== 'undefined') { console.warn(`Request failed with status ${response.status}. Retrying (${retryCount}/${maxRetries}) in ${Math.round(backoffTime)}ms...`); } // Wait for backoff time before retrying await new Promise(resolve => setTimeout(resolve, backoffTime)); } catch (error) { // If the error is due to timeout or network issues and we have retries left if (error instanceof Error && (error.name === 'AbortError' || error.name === 'TypeError') && retryCount < maxRetries) { retryCount++; const backoffTime = this._getBackoffTime(retryCount, initialBackoff); // Log retry information if console is available if (typeof console !== 'undefined') { console.warn(`Request failed with error: ${error.message}. Retrying (${retryCount}/${maxRetries}) in ${Math.round(backoffTime)}ms...`); } // Wait for backoff time before retrying await new Promise(resolve => setTimeout(resolve, backoffTime)); } else { // If we're out of retries or it's a non-retryable error, throw it throw error; } } finally { clearTimeout(timeoutId); } } } /** * Calculate backoff time with exponential backoff and jitter * @param retryCount Current retry attempt number * @param initialBackoff Initial backoff time in ms * @returns Backoff time in ms * @private */ _getBackoffTime(retryCount, initialBackoff) { const backoff = initialBackoff * Math.pow(2, retryCount); // Add jitter (random value between 0 and backoff/2) return backoff + Math.random() * (backoff / 2); } /** * Generate an image and wait for the result * * @param modelId Model ID to use for prediction * @param input Input parameters for the prediction * @param options Additional fetch options */ async run(modelId, input, options) { const prediction = await this.create(modelId, input, options); return prediction.wait(); } /** * Create a prediction without waiting for it to complete * * @param modelId Model ID to use for prediction * @param input Input parameters for the prediction * @param options Additional fetch options */ async create(modelId, input, options) { // Build URL with webhook if provided in options let url = `${modelId}`; if (options === null || options === void 0 ? void 0 : options.webhook) { url += `?webhook=${options.webhook}`; } const response = await this.fetchWithTimeout(url, { method: 'POST', body: JSON.stringify(input), ...options }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to create prediction: ${response.status} ${errorText}`); } const data = await response.json(); if (data.code !== 200) { throw new Error(`Failed to create prediction: ${data.code} ${data}`); } return new Prediction(data.data, this); } async getPrediction(predictionId) { const response = await this.fetchWithTimeout(`predictions/${predictionId}/result`); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to get prediction: ${response.status} ${errorText}`); } const data = await response.json(); if (data.code !== 200) { throw new Error(`Failed to get prediction: ${data.code} ${data}`); } return new Prediction(data.data, this); } /** * Upload a file (binary) to the /media/upload/binary endpoint * @param filePath Absolute path to the file to upload * @returns The API response JSON */ /** * Upload a file (binary) to the /media/upload/binary endpoint (browser Blob version) * @param file Blob to upload * @returns The API response JSON */ async upload(file, options) { const form = new FormData(); if (options === null || options === void 0 ? void 0 : options.filename) { form.append('file', file, options.filename); } else { form.append('file', file); } // Only set Authorization header; browser will set Content-Type if (options == null) { options = { isUpload: true }; } else { options.isUpload = true; } const response = await this.fetchWithTimeout('media/upload/binary', { method: 'POST', body: form, ...options }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to upload file: ${response.status} ${errorText}`); } const resp = await response.json(); return resp.data.download_url; } } exports.WaveSpeed = WaveSpeed; // Export default and named exports for different import styles exports.default = WaveSpeed; // Add browser global for UMD-style usage if (typeof window !== 'undefined') { window.WaveSpeed = WaveSpeed; }