n8n-nodes-wavespeed
Version:
N8N nodes for WaveSpeed AI API - multimodal AI models for text-to-image, image-to-image, text-to-video, and image-to-video generation
676 lines (675 loc) • 33.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WaveSpeedTaskSubmit = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const WaveSpeedClient_1 = require("../../utils/WaveSpeedClient");
class WaveSpeedTaskSubmit {
constructor() {
this.description = {
displayName: 'WaveSpeed Task Submit',
name: 'waveSpeedTaskSubmit',
icon: 'file:WaveSpeedTaskSubmit.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["category"] + " - " + $parameter["model"]}}',
description: 'Submit tasks to WaveSpeed AI models with dynamic model selection and parameter rendering',
documentationUrl: 'https://wavespeed.ai/docs',
defaults: {
name: 'WaveSpeed Task Submit',
},
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
credentials: [
{
name: 'wavespeedApi',
required: true,
},
],
properties: [
{
displayName: 'Model Category',
name: 'category',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getModelCategories',
},
default: '',
required: true,
description: 'The category of models to choose from',
},
{
displayName: 'Model',
name: 'model',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['category'],
loadOptionsMethod: 'getModels',
},
default: '',
required: true,
description: 'The specific model to use.',
displayOptions: {
hide: {
category: [''],
},
},
},
{
displayName: 'Required Parameters',
name: 'requiredParameters',
type: 'resourceMapper',
default: {
value: null,
},
noDataExpression: true,
required: false,
typeOptions: {
loadOptionsDependsOn: ['model'],
resourceMapper: {
resourceMapperMethod: 'getRequiredParameterColumns',
mode: 'add',
fieldWords: {
singular: 'required parameter',
plural: 'required parameters',
},
addAllFields: true,
noFieldsError: 'No required parameters for this model',
supportAutoMap: false,
},
},
displayOptions: {
hide: {
model: [''],
},
},
description: '⚙️ Required parameters for this model. These must be configured.',
},
{
displayName: 'Optional Parameters',
name: 'optionalParameters',
type: 'resourceMapper',
default: {
value: null,
mappingMode: 'defineBelow',
},
required: false,
typeOptions: {
loadOptionsDependsOn: ['model'],
resourceMapper: {
resourceMapperMethod: 'getOptionalParameterColumns',
mode: 'add',
fieldWords: {
singular: 'optional parameter',
plural: 'optional parameters',
},
addAllFields: false,
multiKeyMatch: false,
supportAutoMap: false,
noFieldsError: 'No optional parameters available for this model',
},
},
displayOptions: {
hide: {
model: [''],
},
},
description: '⚙️ Configure optional parameters. Click to add individual parameters as needed.',
},
{
displayName: 'Execution Mode',
name: 'executionMode',
type: 'options',
options: [
{
name: 'Submit Task Only',
value: 'submit',
description: 'Submit task and return task ID immediately',
},
{
name: 'Wait for Completion',
value: 'wait',
description: 'Submit task and wait for completion',
},
],
default: 'submit',
description: 'Whether to wait for task completion or return immediately',
},
{
displayName: 'Polling Options',
name: 'pollingOptions',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
executionMode: ['wait'],
},
},
options: [
{
displayName: 'Max Wait Time (minutes)',
name: 'maxWaitTime',
type: 'number',
default: 5,
description: 'Maximum time to wait for completion in minutes',
},
{
displayName: 'Poll Interval (seconds)',
name: 'pollInterval',
type: 'number',
default: 5,
description: 'How often to check task status in seconds',
},
{
displayName: 'Max Retries (Check Status Errors)',
name: 'maxRetries',
type: 'number',
default: 20,
description: 'Maximum number of retries when status check fails (for network/server errors)',
typeOptions: {
minValue: 1,
maxValue: 100
}
},
],
},
],
};
this.methods = {
loadOptions: {
async getModelCategories() {
const categories = await WaveSpeedClient_1.WaveSpeedClient.getModelCategories();
return categories;
},
async getModels() {
const category = this.getCurrentNodeParameter('category');
if (!category) {
return [];
}
try {
const models = await WaveSpeedClient_1.WaveSpeedClient.getModels(category);
return models;
}
catch (error) {
return [];
}
},
async getModelParameters() {
const modelId = this.getCurrentNodeParameter('model');
if (!modelId) {
return [];
}
try {
const parameters = await WaveSpeedClient_1.WaveSpeedClient.getModelParameters(modelId);
// Convert to option format, display parameter information
return parameters.map(param => ({
name: `${param.displayName} (${param.type})${param.required ? ' *' : ''}`,
value: param.name,
description: `${param.description || 'No description'} | Default: ${param.default || 'none'}`,
}));
}
catch (error) {
return [{
name: 'Error loading parameters',
value: 'error',
description: 'Failed to load model parameters. Check console for details.',
}];
}
},
},
resourceMapping: {
// Resource Mapper method: Dynamically generate required parameter field mapping based on selected model
async getRequiredParameterColumns() {
const modelId = this.getCurrentNodeParameter('model');
if (!modelId) {
return { fields: [] };
}
try {
const parameters = await WaveSpeedClient_1.WaveSpeedClient.getModelParameters(modelId);
// Only process required parameters
const requiredParameters = parameters.filter(param => param.required === true);
// Convert required parameters to ResourceMapperField format
const fields = requiredParameters.map(param => {
// Build field display name, including type and required information
let displayName = param.displayName || param.name;
displayName += ` (${param.type.toUpperCase()})`;
if (param.required) {
displayName += ' *';
}
// Build field description information (will be displayed in UI tooltip)
let displayDescription = param.description || '';
if (param.type === 'collection') {
displayDescription += displayDescription ? ' | ' : '';
displayDescription += 'Please enter JSON format (e.g., {"key": "value"} for objects or ["item1", "item2"] for arrays)';
}
if (param.default !== undefined && param.default !== null) {
displayDescription += displayDescription ? ' | ' : '';
displayDescription += `Default: ${JSON.stringify(param.default)}`;
}
const field = {
id: param.name,
displayName: displayName,
required: param.required || false,
defaultMatch: false, // Not used as matching field
canBeUsedToMatch: false, // Cannot be used for matching
display: true,
type: WaveSpeedTaskSubmit.convertParameterTypeToFieldType(param.type),
};
// Note: ResourceMapperField does not support defaultValue and description properties
// Default value information is already included in displayName
// Add options for options type
if (param.type === 'options' && param.options && param.options.length > 0) {
field.options = param.options.map(opt => ({
name: opt.name,
value: opt.value,
description: opt.description,
}));
}
return field;
});
return { fields };
}
catch (error) {
return { fields: [] };
}
},
// Resource Mapper method: Dynamically generate optional parameter field mapping based on selected model
async getOptionalParameterColumns() {
const modelId = this.getCurrentNodeParameter('model');
if (!modelId) {
return { fields: [] };
}
try {
const parameters = await WaveSpeedClient_1.WaveSpeedClient.getModelParameters(modelId);
// Only process optional parameters
const optionalParameters = parameters.filter(param => param.required !== true);
// Convert optional parameters to ResourceMapperField format
const fields = optionalParameters.map(param => {
// Build field display name, including type information
let displayName = param.displayName || param.name;
displayName += ` (${param.type.toUpperCase()})`;
// If there is a default value, mark it in the display name
if (param.default !== undefined && param.default !== null) {
displayName += ` [Default: ${JSON.stringify(param.default)}]`;
}
const field = {
id: param.name,
displayName: displayName,
required: false, // Optional parameters are not required
defaultMatch: false, // Not used as matching field
canBeUsedToMatch: false, // Cannot be used for matching
display: true,
type: WaveSpeedTaskSubmit.convertParameterTypeToFieldType(param.type),
};
// Add options for options type
if (param.type === 'options' && param.options && param.options.length > 0) {
field.options = param.options.map(opt => ({
name: opt.name || opt.value,
value: opt.value,
description: opt.description || '',
}));
}
return field;
});
return { fields };
}
catch (error) {
return { fields: [] };
}
},
},
};
}
// Helper method to check if a parameter value is empty (more comprehensive than simple null/undefined check)
static isParameterValueEmpty(value) {
if (value === undefined || value === null) {
return true;
}
// Check for empty string (including whitespace-only strings)
if (typeof value === 'string') {
return value.trim() === '';
}
// Check for empty arrays
if (Array.isArray(value)) {
return value.length === 0;
}
// Check for empty objects (but not Date objects or other special objects)
if (typeof value === 'object' && value.constructor === Object) {
return Object.keys(value).length === 0;
}
return false;
}
// Helper method to convert parameter values to appropriate types based on parameter schema
static async convertParameterValue(value, parameterName, modelId) {
// Handle empty values
if (value === undefined || value === null) {
return undefined;
}
// Handle empty strings
if (typeof value === 'string' && value.trim() === '') {
return undefined;
}
// Handle empty arrays and objects
if (Array.isArray(value) && value.length === 0) {
return undefined;
}
if (typeof value === 'object' && value.constructor === Object && Object.keys(value).length === 0) {
return undefined;
}
try {
// Get parameter schema to understand the expected type
const parameters = await WaveSpeedClient_1.WaveSpeedClient.getModelParameters(modelId);
const parameter = parameters.find(p => p.name === parameterName);
if (parameter) {
return this.convertValueByType(value, parameter.type, parameter);
}
}
catch (error) {
console.warn('Failed to get parameter schema, using generic conversion:', error);
}
// Fallback to generic conversion (only for string values)
if (typeof value === 'string') {
return this.convertValueGeneric(value);
}
// For non-string values without schema, return as is
return value;
}
// Helper method to convert value based on specific type
static convertValueByType(value, type, parameter) {
// If value is not a string, try to handle it directly based on type
if (typeof value !== 'string') {
switch (type) {
case 'boolean':
return Boolean(value);
case 'number':
if (typeof value === 'number')
return value;
if (typeof value === 'boolean')
return value ? 1 : 0;
return Number(value);
case 'collection':
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
return value;
}
break;
}
// For other types or if conversion fails, convert to string first
value = String(value);
}
const stringValue = value.trim();
switch (type) {
case 'boolean':
if (stringValue.toLowerCase() === 'true')
return true;
if (stringValue.toLowerCase() === 'false')
return false;
// Convert numbers to boolean (0 = false, non-zero = true)
if (/^-?\d+(\.\d+)?$/.test(stringValue)) {
return parseFloat(stringValue) !== 0;
}
return Boolean(stringValue);
case 'number':
if (/^-?\d+$/.test(stringValue)) {
return parseInt(stringValue, 10);
}
if (/^-?\d*\.\d+$/.test(stringValue)) {
return parseFloat(stringValue);
}
throw new Error(`Invalid number format: "${stringValue}"`);
case 'options':
// For options, validate against available values
if (parameter.options) {
const validOption = parameter.options.find((opt) => opt.value === stringValue || opt.name === stringValue);
if (validOption) {
return validOption.value;
}
throw new Error(`Invalid option: "${stringValue}". Valid options: ${parameter.options.map((opt) => opt.name).join(', ')}`);
}
return stringValue;
case 'collection':
// Try to parse as JSON for arrays/objects
if ((stringValue.startsWith('[') && stringValue.endsWith(']')) ||
(stringValue.startsWith('{') && stringValue.endsWith('}'))) {
try {
return JSON.parse(stringValue);
}
catch {
throw new Error(`Invalid JSON format for collection parameter: "${stringValue}"`);
}
}
return stringValue;
case 'string':
default:
return stringValue;
}
}
// Generic conversion for when parameter schema is not available
static convertValueGeneric(value) {
const stringValue = value.trim();
// Boolean conversion
if (stringValue.toLowerCase() === 'true')
return true;
if (stringValue.toLowerCase() === 'false')
return false;
// Number conversion
if (/^-?\d+$/.test(stringValue)) {
return parseInt(stringValue, 10);
}
if (/^-?\d*\.\d+$/.test(stringValue)) {
return parseFloat(stringValue);
}
// JSON conversion (for arrays/objects)
if ((stringValue.startsWith('[') && stringValue.endsWith(']')) ||
(stringValue.startsWith('{') && stringValue.endsWith('}'))) {
try {
return JSON.parse(stringValue);
}
catch {
// If JSON parsing fails, return as string
}
}
return stringValue;
}
// Helper method: Convert parameter type to ResourceMapper supported field type
static convertParameterTypeToFieldType(paramType) {
switch (paramType.toLowerCase()) {
case 'string':
case 'text':
return 'string';
case 'number':
case 'integer':
case 'float':
return 'number';
case 'boolean':
return 'boolean';
case 'datetime':
case 'timestamp':
return 'dateTime';
case 'time':
return 'time';
case 'array':
return 'array';
case 'object':
return 'object';
case 'options':
case 'enum':
return 'options';
case 'collection':
// collection type uses string in ResourceMapper, users need to input JSON format
return 'string';
default:
return 'string';
}
}
async execute() {
const items = this.getInputData();
const returnData = [];
for (let i = 0; i < items.length; i++) {
try {
const category = this.getNodeParameter('category', i);
const model = this.getNodeParameter('model', i);
const executionMode = this.getNodeParameter('executionMode', i);
const credentials = await this.getCredentials('wavespeedApi');
const apiKey = credentials.apiKey;
const requestData = {};
// Handle separated parameter system: required parameters + optional parameters
// Key optimization points:
// 1. Empty parameters are not passed (ignore parameters not filled by user)
// 2. Smart type conversion based on model parameter schema
// 3. Default values are handled by backend, frontend does not preset
try {
// Get all available parameters of the model for validation and type conversion
const availableParameters = await WaveSpeedClient_1.WaveSpeedClient.getModelParameters(model);
// No longer preset default values, let backend add default values based on model information
// Only process parameter values explicitly provided by user
// 1. Process required parameters (Resource Mapper)
// Required parameters must have values, empty values will throw errors
const requiredParametersValue = this.getNodeParameter('requiredParameters', i);
if (requiredParametersValue && requiredParametersValue.value) {
for (const [paramName, paramValue] of Object.entries(requiredParametersValue.value)) {
const paramDef = availableParameters.find(p => p.name === paramName);
if (paramDef) {
// Required parameters must have values
if (paramValue === undefined || paramValue === null || paramValue === '') {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Required parameter '${paramName}' is missing or empty`, { itemIndex: i });
}
try {
const convertedValue = await WaveSpeedTaskSubmit.convertParameterValue(paramValue, paramName, model);
if (convertedValue !== undefined) {
requestData[paramName] = convertedValue;
}
}
catch (error) {
requestData[paramName] = paramValue;
}
}
}
}
// 2. Process optional parameters (Resource Mapper) - Smart type conversion
// Core optimization: Only pass non-empty optional parameters, completely ignore empty parameters
// This way backend can use model's built-in default values
const optionalParametersValue = this.getNodeParameter('optionalParameters', i);
if (optionalParametersValue && optionalParametersValue.value) {
for (const [paramName, paramValue] of Object.entries(optionalParametersValue.value)) {
const paramDef = availableParameters.find(p => p.name === paramName);
// Check if parameter value is empty (including stricter empty value checks)
const isEmpty = WaveSpeedTaskSubmit.isParameterValueEmpty(paramValue);
if (!isEmpty) {
if (paramDef) {
// Verify that parameter is indeed optional
if (paramDef.required) {
}
try {
const convertedValue = await WaveSpeedTaskSubmit.convertParameterValue(paramValue, paramName, model);
// Check again if converted value is empty
if (convertedValue !== undefined && convertedValue !== null &&
!WaveSpeedTaskSubmit.isParameterValueEmpty(convertedValue)) {
requestData[paramName] = convertedValue;
}
else {
}
}
catch (error) {
// For conversion failures, if original value is not empty, use original value
if (!WaveSpeedTaskSubmit.isParameterValueEmpty(paramValue)) {
requestData[paramName] = paramValue;
}
}
}
else {
// For unknown parameters, also allow usage (support custom parameters)
try {
const convertedValue = await WaveSpeedTaskSubmit.convertParameterValue(paramValue, paramName, model);
if (convertedValue !== undefined && convertedValue !== null &&
!WaveSpeedTaskSubmit.isParameterValueEmpty(convertedValue)) {
requestData[paramName] = convertedValue;
}
else {
}
}
catch (error) {
// For conversion failures, if original value is not empty, use original value
if (!WaveSpeedTaskSubmit.isParameterValueEmpty(paramValue)) {
requestData[paramName] = paramValue;
}
}
}
}
else {
}
}
}
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error processing model parameters: ${error}`, { itemIndex: i });
}
// Submit task
const taskResult = await WaveSpeedClient_1.WaveSpeedClient.submitTask(model, requestData, apiKey);
let result = {
task_id: taskResult.id,
model: taskResult.model,
input: taskResult.input,
status: taskResult.status,
created_at: taskResult.created_at,
category,
model_id: model,
parameters: requestData,
};
// If task is already completed (synchronous return), include results directly
if (taskResult.status === 'completed') {
result.outputs = taskResult.outputs;
result.has_nsfw_contents = taskResult.has_nsfw_contents;
result.timings = taskResult.timings;
}
// If wait for completion mode is selected and task is not completed
if (executionMode === 'wait' && taskResult.status !== 'completed' && taskResult.status !== 'failed') {
const pollingOptions = this.getNodeParameter('pollingOptions', i, {});
const maxWaitTime = (pollingOptions.maxWaitTime || 5) * 60 * 1000; // Convert to milliseconds
const pollInterval = (pollingOptions.pollInterval || 5) * 1000; // Convert to milliseconds
const maxRetries = pollingOptions.maxRetries || 20; // Default 20 retries
try {
const completedTask = await WaveSpeedClient_1.WaveSpeedClient.waitForTaskCompletion(taskResult.id, apiKey, maxWaitTime, pollInterval, maxRetries);
result = {
...result,
status: completedTask.status,
outputs: completedTask.outputs,
has_nsfw_contents: completedTask.has_nsfw_contents,
timings: completedTask.timings,
error: completedTask.error,
};
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Task completion error: ${error}`, { itemIndex: i });
}
}
// If task failed
if (taskResult.status === 'failed') {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Task failed: ${taskResult.error || 'Unknown error'}`, { itemIndex: i });
}
returnData.push({
json: result,
pairedItem: { item: i },
});
}
catch (error) {
if (this.continueOnFail()) {
let errorMessage = 'Unknown error occurred';
if (error instanceof Error) {
errorMessage = error.message;
}
else if (typeof error === 'string') {
errorMessage = error;
}
returnData.push({
json: { error: errorMessage },
pairedItem: { item: i },
});
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}
exports.WaveSpeedTaskSubmit = WaveSpeedTaskSubmit;