wavespeed
Version:
WaveSpeed Client SDK for Wavespeed API
286 lines (285 loc) • 11.3 kB
JavaScript
/**
* 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;
}
;