UNPKG

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
"use strict"; 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;