From 81f6f2a72a7ee8c80610047bdae8021e4409f67a Mon Sep 17 00:00:00 2001 From: Leon Date: Wed, 10 Dec 2025 16:57:22 +0800 Subject: [PATCH] Update JS client examples and tests: refine basic usage sample, refresh setup helpers, and align docs --- README.md | 76 ++++++-------- src/index.ts | 217 +++++++++++++++++++++++----------------- tests/setup.ts | 6 +- tests/wavespeed.test.ts | 138 +++++++++++++++++-------- 4 files changed, 253 insertions(+), 184 deletions(-) diff --git a/README.md b/README.md index 876f140..f3379fa 100644 --- a/README.md +++ b/README.md @@ -15,28 +15,15 @@ npm install wavespeed ```javascript const { WaveSpeed } = require('wavespeed'); -// Initialize the client with your API key -const client = new WaveSpeed('YOUR_API_KEY'); +const client = new WaveSpeed(); // reads WAVESPEED_API_KEY from env -// Generate an image and wait for the result async function generateImage() { const prediction = await client.run( - 'wavespeed-ai/flux-dev', - { - prompt: 'A futuristic cityscape with flying cars and neon lights', - size: '1024*1024', - num_inference_steps: 28, - guidance_scale: 5.0, - num_images: 1, - seed: -1, - enable_safety_checker: true - } + 'wavespeed-ai/z-image/turbo', + { prompt: 'A simple red circle on white background' } ); - // Print the generated image URLs - prediction.outputs.forEach((imgUrl, i) => { - console.log(`Image ${i+1}: ${imgUrl}`); - }); + prediction.outputs.forEach((imgUrl) => console.log(imgUrl)); } generateImage().catch(console.error); @@ -47,27 +34,15 @@ generateImage().catch(console.error); ```typescript import WaveSpeed from 'wavespeed'; -// Initialize the client with your API key -const client = new WaveSpeed('YOUR_API_KEY'); +const client = new WaveSpeed(); // reads WAVESPEED_API_KEY from env -// Generate an image and wait for the result async function generateImage(): Promise { - const input: Record = { - prompt: 'A futuristic cityscape with flying cars and neon lights', - size: '1024*1024', - num_inference_steps: 28, - guidance_scale: 5.0, - num_images: 1, - seed: -1, - enable_safety_checker: true - }; - - const prediction = await client.run('wavespeed-ai/flux-dev', input); - - // Print the generated image URLs - prediction.outputs.forEach((imgUrl, i) => { - console.log(`Image ${i+1}: ${imgUrl}`); - }); + const prediction = await client.run( + 'wavespeed-ai/z-image/turbo', + { prompt: 'A simple red circle on white background' } + ); + + prediction.outputs.forEach((imgUrl) => console.log(imgUrl)); } generateImage().catch(console.error); @@ -86,11 +61,7 @@ const client = new WaveSpeed('YOUR_API_KEY'); async function generateWithManualPolling(): Promise { // Create a prediction without waiting const prediction = await client.create('wavespeed-ai/flux-dev', { - prompt: 'A beautiful mountain landscape at sunset', - size: '1024*1024', - num_inference_steps: 28, - guidance_scale: 5.0, - num_images: 1 + prompt: 'A beautiful mountain landscape at sunset' }); console.log(`Prediction created with ID: ${prediction.id}`); @@ -129,23 +100,23 @@ generateWithManualPolling().catch(console.error); new WaveSpeed(apiKey?: string, options?: { baseUrl?: string, pollInterval?: number, - timeout?: number + timeout?: number // overall wait timeout (seconds) }) ``` #### Parameters: - `apiKey` (string): Your WaveSpeed API key - `options` (object, optional): - - `baseUrl` (string): API base URL (default: 'https://api.wavespeed.ai/api/v2/') + - `baseUrl` (string): API base URL without path (default: 'https://api.wavespeed.ai'; e.g. `https://your.domain` if self-hosted) - `pollInterval` (number): Interval in seconds for polling prediction status (default: 1) - - `timeout` (number): Timeout in seconds for API requests (default: 60) + - `timeout` (number): Overall wait timeout in seconds for a run (default: 36000) ### Methods #### run ```typescript -run(modelId: string, input: Record, options?: RequestOptions): Promise +run(modelId: string, input: Record, options?: { pollInterval?: number; timeout?: number }): Promise ``` Generate an image and wait for the result. @@ -153,11 +124,19 @@ Generate an image and wait for the result. #### create ```typescript -create(modelId: string, input: Record, options?: RequestOptions): Promise +create(modelId: string, input: Record, options?: { pollInterval?: number; timeout?: number }): Promise ``` Create a prediction without waiting for it to complete. +#### upload + +```typescript +upload(filePath: string): Promise +``` + +Upload a file by local path (Node) and get a `download_url` for use as model input. + ### Prediction Model The Prediction object contains information about an image generation job: @@ -178,15 +157,16 @@ prediction.executionTime // Time taken to execute the prediction in milliseconds #### Methods ```typescript -prediction.wait(): Promise // Wait for the prediction to complete +prediction.wait(pollIntervalSeconds?: number, timeoutSeconds?: number): Promise // Wait for the prediction to complete with optional overrides prediction.reload(): Promise // Reload the prediction status ``` ## Environment Variables - `WAVESPEED_API_KEY`: Your WaveSpeed API key +- `WAVESPEED_BASE_URL`: API base URL without path (default: https://api.wavespeed.ai) - `WAVESPEED_POLL_INTERVAL`: Interval in seconds for polling prediction status (default: 1) -- `WAVESPEED_TIMEOUT`: Timeout in seconds for API requests (default: 60) +- `WAVESPEED_TIMEOUT`: Overall wait timeout in seconds for a run (default: 36000) ## License diff --git a/src/index.ts b/src/index.ts index 47afa3e..a3ac193 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,15 +26,22 @@ export interface UploadFileResp { } /** - * Request options for fetch + * User-facing run options */ -export interface RequestOptions extends RequestInit { - timeout?: number; - maxRetries?: number; - webhook?: string; - isUpload?: boolean; +export interface RunOptions { + timeout?: number; // overall wait timeout in seconds + pollInterval?: number; // per-call poll interval override } +// Internal options used inside the SDK +type InternalRequestOptions = RunOptions & + RequestInit & { + webhook?: string; + isUpload?: boolean; + maxRetries?: number; + waitTimeout?: number; // deprecated alias for timeout + }; + /** * Prediction model representing an image generation job */ @@ -69,7 +76,9 @@ export class Prediction { /** * Wait for the prediction to complete */ - async wait(): Promise { + async wait(pollIntervalSeconds?: number, totalTimeoutSeconds?: number): Promise { + const startedAt = Date.now(); + const interval = pollIntervalSeconds ?? this.client.pollInterval; if (this.status === 'completed' || this.status === 'failed') { return this; } @@ -77,11 +86,18 @@ export class Prediction { return new Promise((resolve, reject) => { const checkStatus = async () => { try { + if (typeof totalTimeoutSeconds === 'number') { + const elapsedSeconds = (Date.now() - startedAt) / 1000; + if (elapsedSeconds >= totalTimeoutSeconds) { + throw new Error(`Prediction timed out after ${totalTimeoutSeconds} seconds`); + } + } + const updated = await this.reload(); if (updated.status === 'completed' || updated.status === 'failed') { resolve(updated); } else { - setTimeout(checkStatus, this.client.pollInterval * 1000); + setTimeout(checkStatus, interval * 1000); } } catch (error) { reject(error); @@ -118,9 +134,10 @@ export class Prediction { */ export class WaveSpeed { private apiKey: string; - private baseUrl: string = 'https://api.wavespeed.ai/api/v3/'; + private baseUrl: string; readonly pollInterval: number; - readonly timeout: number; + readonly timeout?: number; // overall wait timeout (seconds) + private readonly requestTimeout: number; // per-request timeout (seconds), internal /** * Create a new WaveSpeed client @@ -128,14 +145,15 @@ export class WaveSpeed { * @param apiKey Your WaveSpeed API key (or set WAVESPEED_API_KEY environment variable) * @param options Additional client options */ - constructor(apiKey?: string, options: { - baseUrl?: string, - pollInterval?: number, - timeout?: number - } = {}) { - // Browser-friendly environment variable handling + constructor( + apiKey?: string, + options: { + baseUrl?: string; + pollInterval?: number; + timeout?: number; // overall wait timeout + } = {}, + ) { const getEnvVar = (name: string): string | undefined => { - // Try to get from process.env for Node.js environments if (typeof process !== 'undefined' && process.env && process.env[name]) { return process.env[name]; } @@ -143,17 +161,39 @@ export class WaveSpeed { }; 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; - } + const envBaseUrl = getEnvVar('WAVESPEED_BASE_URL'); + this.baseUrl = (options.baseUrl || envBaseUrl || 'https://api.wavespeed.ai').replace(/\/+$/, ''); + + const envPoll = Number(getEnvVar('WAVESPEED_POLL_INTERVAL')); + const envTimeout = Number(getEnvVar('WAVESPEED_TIMEOUT')); // overall wait timeout + const envRequestTimeout = Number(getEnvVar('WAVESPEED_REQUEST_TIMEOUT')); // per-request timeout (internal) + + this.pollInterval = options.pollInterval ?? (Number.isFinite(envPoll) ? envPoll : 1); + this.timeout = options.timeout ?? (Number.isFinite(envTimeout) ? envTimeout : 36000); // align with Python default wait + this.requestTimeout = Number.isFinite(envRequestTimeout) ? envRequestTimeout : 120; + } - this.pollInterval = options.pollInterval || Number(getEnvVar('WAVESPEED_POLL_INTERVAL')) || 0.5; - this.timeout = options.timeout || Number(getEnvVar('WAVESPEED_TIMEOUT')) || 120; + private buildUrl(path: string): string { + const cleanPath = path.replace(/^\/+/, ''); + return `${this.baseUrl}/api/v3/${cleanPath}`; + } + + private getHeaders(isUpload?: boolean, extra?: HeadersInit): HeadersInit { + if (isUpload) { + return { + Authorization: `Bearer ${this.apiKey}`, + ...(extra || {}), + }; + } + return { + Authorization: `Bearer ${this.apiKey}`, + 'content-type': 'application/json', + ...(extra || {}), + }; } /** @@ -162,88 +202,66 @@ export class WaveSpeed { * @param path API path * @param options Fetch options */ - async fetchWithTimeout(path: string, options: RequestOptions = {}): Promise { - const { timeout = this.timeout * 1000, ...fetchOptions } = options; - - // Ensure headers exist - if (options.isUpload) { - fetchOptions.headers = { - 'Authorization': `Bearer ${this.apiKey}`, - ...(fetchOptions.headers || {}), - }; - - } 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 + async fetchWithTimeout(path: string, options: InternalRequestOptions = {}): Promise { + const { + timeout: _overallTimeout, // strip out overall wait timeout, not used here + pollInterval: _pollInterval, + waitTimeout: _waitTimeout, + maxRetries, + isUpload, + webhook: _webhook, + ...fetchOptions + } = options; + + const perRequestTimeoutSeconds = this.requestTimeout; + const timeout = perRequestTimeoutSeconds * 1000; + const method = (fetchOptions.method || 'GET').toUpperCase(); + + fetchOptions.headers = this.getHeaders(isUpload, fetchOptions.headers); + + const maxRetriesVal = maxRetries ?? 3; + const initialBackoff = 1000; let retryCount = 0; - // Function to determine if a response should be retried const shouldRetry = (response: Response): boolean => { - // 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 url = new URL(path.startsWith('/') ? path.substring(1) : path, this.baseUrl).toString(); - + const url = this.buildUrl(path); const response = await fetch(url, { ...fetchOptions, - signal: controller.signal + signal: controller.signal, }); - // If the response is successful or we've used all retries, return it - if (response.ok || !shouldRetry(response) || retryCount >= maxRetries) { + if (response.ok || !shouldRetry(response) || retryCount >= maxRetriesVal) { 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...`); + console.warn(`Request failed with status ${response.status}. Retrying (${retryCount}/${maxRetriesVal}) in ${Math.round(backoffTime)}ms...`); } - - // Wait for backoff time before retrying - await new Promise(resolve => setTimeout(resolve, backoffTime)); - + 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 && + if ( + error instanceof Error && (error.name === 'AbortError' || error.name === 'TypeError') && - retryCount < maxRetries) { - + retryCount < maxRetriesVal && + method === 'GET' + ) { 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...`); + console.warn(`Request failed with error: ${error.message}. Retrying (${retryCount}/${maxRetriesVal}) in ${Math.round(backoffTime)}ms...`); } - - // Wait for backoff time before retrying - await new Promise(resolve => setTimeout(resolve, backoffTime)); - + 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 { @@ -272,9 +290,11 @@ export class WaveSpeed { * @param input Input parameters for the prediction * @param options Additional fetch options */ - async run(modelId: string, input: Record, options?: RequestOptions): Promise { + async run(modelId: string, input: Record, options?: RunOptions): Promise { const prediction = await this.create(modelId, input, options); - return prediction.wait(); + const pollInterval = options?.pollInterval ?? this.pollInterval; + const waitTimeout = options?.timeout ?? this.timeout; + return prediction.wait(pollInterval, waitTimeout); } /** @@ -284,18 +304,16 @@ export class WaveSpeed { * @param input Input parameters for the prediction * @param options Additional fetch options */ - async create(modelId: string, input: Record, options?: RequestOptions): Promise { - - // Build URL with webhook if provided in options - let url = `${modelId}`; + async create(modelId: string, input: Record, options?: InternalRequestOptions): Promise { + let path = `${modelId}`; if (options?.webhook) { - url += `?webhook=${options.webhook}`; + path += `?webhook=${options.webhook}`; } - const response = await this.fetchWithTimeout(url, { + const response = await this.fetchWithTimeout(path, { method: 'POST', body: JSON.stringify(input), - ...options + ...options, }); if (!response.ok) { @@ -320,17 +338,32 @@ export class WaveSpeed { * @param file Blob to upload * @returns The API response JSON */ - async upload(file: Blob, options?: RequestOptions): Promise { - const form = new FormData(); - form.append('file', file); - // Only set Authorization header; browser will set Content-Type - if (options == null) { - options = { isUpload: true } + async upload(filePath: string): Promise { + // Align with Python: accept file path (Node environment) + let fs: any; + let path: any; + try { + fs = require('fs'); + path = require('path'); + } catch (err) { + throw new Error('File path uploads are only supported in Node environments.'); } + + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath}`); + } + + const data = fs.readFileSync(filePath); + const filename = path.basename(filePath); + const form = new FormData(); + const blob = new Blob([data]); + form.append('file', blob, filename); + // Ensure upload headers; browser will set Content-Type + const uploadOptions: InternalRequestOptions = { isUpload: true }; const response = await this.fetchWithTimeout('media/upload/binary', { method: 'POST', body: form, - ...options + ...uploadOptions }); if (!response.ok) { const errorText = await response.text(); diff --git a/tests/setup.ts b/tests/setup.ts index 67fa294..fc44775 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,7 +1,5 @@ -// This file contains setup code for Jest tests - -// Mock timers for testing -// jest.useFakeTimers(); +// Jest setup for Node environment: provide fetch and clean mocks. +// Node.js 18+ has built-in fetch, no need to import // Reset all mocks after each test afterEach(() => { diff --git a/tests/wavespeed.test.ts b/tests/wavespeed.test.ts index 1634c31..527b3e3 100644 --- a/tests/wavespeed.test.ts +++ b/tests/wavespeed.test.ts @@ -1,4 +1,7 @@ -import { WaveSpeed, Prediction, RequestOptions } from '../src'; +import { WaveSpeed, Prediction } from '../src'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; // Mock fetch const originalFetch = global.fetch; @@ -6,7 +9,14 @@ const originalFetch = global.fetch; describe('WaveSpeed Client', () => { // Save original environment and fetch const originalEnv = process.env; + const originalConsoleWarn = console.warn; + const mockJson = (data: any, status = 200) => + new Response(JSON.stringify(data), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + beforeEach(() => { // Reset mocks jest.resetAllMocks(); @@ -19,25 +29,28 @@ describe('WaveSpeed Client', () => { delete process.env.WAVESPEED_API_KEY; delete process.env.WAVESPEED_POLL_INTERVAL; delete process.env.WAVESPEED_TIMEOUT; + delete process.env.WAVESPEED_BASE_URL; }); afterAll(() => { // Restore environment and fetch process.env = originalEnv; global.fetch = originalFetch; + console.warn = originalConsoleWarn; }); describe('Constructor', () => { - test('should initialize with provided API key', () => { + test('should initialize with provided API key and defaults', () => { const client = new WaveSpeed('test-api-key'); expect(client).toHaveProperty('apiKey', 'test-api-key'); - expect(client).toHaveProperty('baseUrl', 'https://api.wavespeed.ai/api/v2/'); + expect(client).toHaveProperty('pollInterval', 1); }); test('should initialize with API key from environment variable', () => { process.env.WAVESPEED_API_KEY = 'env-api-key'; const client = new WaveSpeed(); - expect(client).toHaveProperty('pollInterval', 0.5); + expect(client).toHaveProperty('pollInterval', 1); + expect(client).toHaveProperty('timeout', 36000); }); test('should throw error if no API key is provided', () => { @@ -87,11 +100,11 @@ describe('WaveSpeed Client', () => { // Verify fetch was called with correct parameters expect(global.fetch).toHaveBeenCalledWith( - 'https://api.wavespeed.ai/api/v2/test-path', + 'https://api.wavespeed.ai/api/v3/test-path', expect.objectContaining({ headers: expect.objectContaining({ 'Authorization': 'Bearer test-api-key', - 'Content-Type': 'application/json' + 'content-type': 'application/json' }), signal: expect.any(AbortSignal) }) @@ -102,6 +115,7 @@ describe('WaveSpeed Client', () => { }); test('should handle timeout', async () => { + process.env.WAVESPEED_REQUEST_TIMEOUT = '0.05'; // Mock AbortController const mockAbort = jest.fn(); const mockSignal = { aborted: false } as AbortSignal; @@ -114,14 +128,13 @@ describe('WaveSpeed Client', () => { (global.fetch as jest.Mock).mockImplementation(() => new Promise(() => {})); // Create client with short timeout - const client = new WaveSpeed('test-api-key', { timeout: 0.1 }); - - - // Call fetchWithTimeout (don't await) - const fetchPromise = client.fetchWithTimeout('/test-path'); + const client = new WaveSpeed('test-api-key'); + + // Call fetchWithTimeout (don't await; let timeout trigger abort) + client.fetchWithTimeout('/test-path').catch(() => {}); + + await new Promise(resolve => setTimeout(resolve, 200)); - await new Promise(resolve => setTimeout(resolve, 100)); - // Verify abort was called expect(mockAbort).toHaveBeenCalled(); @@ -134,7 +147,6 @@ describe('WaveSpeed Client', () => { jest.setTimeout(10000); // Mock console.warn to verify logging - const originalConsoleWarn = console.warn; console.warn = jest.fn(); // Create responses - first with 429, then success @@ -172,8 +184,6 @@ describe('WaveSpeed Client', () => { expect.stringContaining('Request failed with status 429') ); - // Restore mocks - console.warn = originalConsoleWarn; jest.setTimeout(5000); // Reset timeout }); @@ -182,7 +192,6 @@ describe('WaveSpeed Client', () => { jest.setTimeout(10000); // Mock console.warn - const originalConsoleWarn = console.warn; console.warn = jest.fn(); // Create responses - first with 503, then success @@ -211,7 +220,6 @@ describe('WaveSpeed Client', () => { expect(response.status).toBe(200); // Restore mocks - console.warn = originalConsoleWarn; jest.setTimeout(5000); // Reset timeout }); @@ -319,15 +327,19 @@ describe('WaveSpeed Client', () => { describe('create method', () => { test('should create a prediction', async () => { // Mock response data - const mockResponseData = {data: - {id: 'pred-123', - model: 'wavespeed-ai/flux-dev', - status: 'processing', - input: { prompt: 'test prompt' }, - outputs: [], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123' }, - has_nsfw_contents: [], - created_at: '2023-01-01T00:00:00Z'} + const mockResponseData = { + code: 200, + message: 'ok', + data: { + id: 'pred-123', + model: 'wavespeed-ai/flux-dev', + status: 'processing', + input: { prompt: 'test prompt' }, + outputs: [], + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123' }, + has_nsfw_contents: [], + created_at: '2023-01-01T00:00:00Z' + } }; // Setup mock response @@ -344,7 +356,7 @@ describe('WaveSpeed Client', () => { // Verify fetch was called with correct parameters expect(global.fetch).toHaveBeenCalledWith( - 'https://api.wavespeed.ai/api/v2/wavespeed-ai/flux-dev', + 'https://api.wavespeed.ai/api/v3/wavespeed-ai/flux-dev', expect.objectContaining({ method: 'POST', body: JSON.stringify(input) @@ -376,25 +388,25 @@ describe('WaveSpeed Client', () => { describe('run method', () => { test('should create a prediction and wait for completion', async () => { // Mock response data for create - const mockCreateResponse = {data:{ + const mockCreateResponse = {code:200,message:'ok',data:{ id: 'pred-123', model: 'wavespeed-ai/flux-dev', status: 'processing', input: { prompt: 'test prompt' }, outputs: [], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123' }, + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123' }, has_nsfw_contents: [], created_at: '2023-01-01T00:00:00Z'} }; // Mock response data for get (completed prediction) - const mockGetResponse = {data: + const mockGetResponse = {code:200,message:'ok',data: {id: 'pred-123', model: 'wavespeed-ai/flux-dev', status: 'completed', input: { prompt: 'test prompt' }, outputs: ['https://example.com/image1.png'], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123' }, + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123' }, has_nsfw_contents: [false], created_at: '2023-01-01T00:00:00Z', executionTime: 5000} @@ -420,7 +432,7 @@ describe('WaveSpeed Client', () => { expect(global.fetch).toHaveBeenCalledTimes(2); expect(global.fetch).toHaveBeenNthCalledWith( 1, - 'https://api.wavespeed.ai/api/v2/wavespeed-ai/flux-dev', + 'https://api.wavespeed.ai/api/v3/wavespeed-ai/flux-dev', expect.objectContaining({ method: 'POST', body: JSON.stringify(input) @@ -429,7 +441,7 @@ describe('WaveSpeed Client', () => { expect(global.fetch).toHaveBeenNthCalledWith( 2, - 'https://api.wavespeed.ai/api/v2/predictions/pred-123/result', + 'https://api.wavespeed.ai/api/v3/predictions/pred-123/result', expect.objectContaining({ headers: expect.objectContaining({ 'Authorization': 'Bearer test-api-key' @@ -445,6 +457,7 @@ describe('WaveSpeed Client', () => { expect(prediction.executionTime).toBe(5000); }); }); + }); describe('Prediction', () => { @@ -468,7 +481,7 @@ describe('Prediction', () => { status: 'completed', input: { prompt: 'test prompt' }, outputs: ['https://example.com/image1.png'], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123' }, + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123' }, has_nsfw_contents: [false], created_at: '2023-01-01T00:00:00Z', executionTime: 5000 @@ -494,7 +507,7 @@ describe('Prediction', () => { status: 'processing', input: { prompt: 'test prompt' }, outputs: [], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123' }, + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123' }, has_nsfw_contents: [], created_at: '2023-01-01T00:00:00Z'} }; @@ -511,7 +524,7 @@ describe('Prediction', () => { status: 'completed', input: { prompt: 'test prompt' }, outputs: ['https://example.com/image1.png'], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123' }, + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123' }, has_nsfw_contents: [false], created_at: '2023-01-01T00:00:00Z', executionTime: 5000} @@ -567,7 +580,7 @@ describe('Prediction', () => { status: 'processing', input: { prompt: 'test prompt' }, outputs: [], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123/result' }, + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123/result' }, has_nsfw_contents: [], created_at: '2023-01-01T00:00:00Z' }}; @@ -579,7 +592,7 @@ describe('Prediction', () => { status: 'completed', input: { prompt: 'test prompt' }, outputs: ['https://example.com/image1.png'], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123/result' }, + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123/result' }, has_nsfw_contents: [false], created_at: '2023-01-01T00:00:00Z', executionTime: 5000 @@ -613,7 +626,7 @@ describe('Prediction', () => { status: 'processing', input: { prompt: 'test prompt' }, outputs: [], - urls: { get: 'https://api.wavespeed.ai/api/v2/predictions/pred-123/result' }, + urls: { get: 'https://api.wavespeed.ai/api/v3/predictions/pred-123/result' }, has_nsfw_contents: [], created_at: '2023-01-01T00:00:00Z' }}; @@ -634,3 +647,48 @@ describe('Prediction', () => { }); }); }); + +describe('Real API (integration) - skipped without WAVESPEED_API_KEY', () => { + test('real run', async () => { + if (!process.env.WAVESPEED_API_KEY) { + console.warn('[real run] skipped because WAVESPEED_API_KEY is not set'); + return; + } + const client = new WaveSpeed(); + const prediction = await client.run( + 'wavespeed-ai/z-image/turbo', + { prompt: 'Test image from js sdk' }, + { pollInterval: 1, timeout: 120 } + ); + console.log('[real run] status=', prediction.status, 'output0=', prediction.outputs?.[0]); + expect(prediction.outputs?.length).toBeGreaterThan(0); + }, 180000); +}); + +describe('Real API (upload) - skipped without WAVESPEED_API_KEY', () => { + test('real upload', async () => { + if (!process.env.WAVESPEED_API_KEY) { + console.warn('[real upload] skipped because WAVESPEED_API_KEY is not set'); + return; + } + const client = new WaveSpeed(); + + // minimal 1x1 PNG written to a temp file (Node upload expects path) + const pngBytes = new Uint8Array([ + 0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a, + 0x00,0x00,0x00,0x0d,0x49,0x48,0x44,0x52, + 0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01, + 0x08,0x02,0x00,0x00,0x00,0x90,0x77,0x53,0xde, + 0x00,0x00,0x00,0x0c,0x49,0x44,0x41,0x54,0x78,0x9c, + 0x63,0xf8,0xcf,0xc0,0x00,0x00,0x00,0x03,0x00,0x01,0x00,0x05,0xfe,0xd4, + 0x00,0x00,0x00,0x00,0x49,0x45,0x4e,0x44,0xae,0x42,0x60,0x82 + ]); + const tmpPath = path.join(os.tmpdir(), `wavespeed-js-upload-${Date.now()}.png`); + fs.writeFileSync(tmpPath, Buffer.from(pngBytes)); + + const url = await client.upload(tmpPath as any); + fs.unlinkSync(tmpPath); + console.log('[real upload] download_url=', url); + expect(url).toBeDefined(); + }, 180000); +});