UNPKG

n8n-nodes-openai-gpt5

Version:

n8n node for OpenAI GPT-5 with PDF processing, web search, and timeout configuration via Responses API

1,039 lines (1,038 loc) 50.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OpenAiGpt5 = void 0; const n8n_workflow_1 = require("n8n-workflow"); class OpenAiGpt5 { constructor() { this.description = { displayName: 'OpenAI GPT-5', name: 'openAiGpt5', icon: 'file:openai.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'Process PDFs and other files with OpenAI GPT-5 using the Responses API', defaults: { name: 'OpenAI GPT-5', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'openAiGpt5Api', required: true, }, ], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'AI Tool Mode', value: 'aiToolMode', description: 'Simplified mode for AI Agent usage', action: 'Process in AI tool mode', }, { name: 'Chat Completion', value: 'chat', description: 'Send a simple text prompt to GPT-5', action: 'Send a text prompt to GPT-5', }, { name: 'Image Analysis', value: 'imageAnalysis', description: 'Analyze images with GPT-5 vision capabilities', action: 'Analyze images with GPT-5', }, { name: 'Process Multiple Files', value: 'multiFile', description: 'Process multiple PDFs and images together', action: 'Process multiple files with GPT-5', }, { name: 'Process with File ID', value: 'processFileId', description: 'Process a previously uploaded file using its ID', action: 'Process a previously uploaded file using its ID', }, { name: 'Upload & Process PDF', value: 'uploadAndProcess', description: 'Upload a PDF and process it with GPT-5', action: 'Upload a PDF and process it with GPT-5', }, ], default: 'chat', }, // Chat Completion fields { displayName: 'Prompt', name: 'chatPrompt', type: 'string', typeOptions: { rows: 4, }, default: '', required: true, displayOptions: { show: { operation: ['chat'], }, }, description: 'The prompt to send to GPT-5', }, // Image Analysis fields { displayName: 'Image Source', name: 'imageSource', type: 'options', options: [ { name: 'Binary Data', value: 'binary', }, { name: 'URL', value: 'url', }, ], default: 'binary', displayOptions: { show: { operation: ['imageAnalysis'], }, }, }, { displayName: 'Binary Property', name: 'imageBinaryProperty', type: 'string', default: 'data', required: true, displayOptions: { show: { operation: ['imageAnalysis'], imageSource: ['binary'], }, }, description: 'Name of the binary property containing the image', }, { displayName: 'Image URL', name: 'imageUrl', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['imageAnalysis'], imageSource: ['url'], }, }, placeholder: 'https://example.com/image.jpg', description: 'URL of the image to analyze', }, { displayName: 'Analysis Prompt', name: 'imagePrompt', type: 'string', typeOptions: { rows: 4, }, default: 'Describe this image in detail.', required: true, displayOptions: { show: { operation: ['imageAnalysis'], }, }, description: 'What to analyze about the image', }, // Multi-file processing fields { displayName: 'PDF Files', name: 'multiPdfFiles', type: 'json', default: '', placeholder: '"file_abc123" or ["file_1", "file_2"] or {{ $json.pdfIds }}', displayOptions: { show: { operation: ['multiFile'], }, }, description: 'PDF file IDs to process. Accepts single ID, array, or expression.', }, { displayName: 'Image Files', name: 'multiImageFiles', type: 'json', default: '', placeholder: '"https://url.jpg" or ["url1", "url2"] or {{ $json.images }}', displayOptions: { show: { operation: ['multiFile'], }, }, description: 'Image URLs or file IDs. Accepts single value, array, or expression.', }, { displayName: 'Multi-File Prompt', name: 'multiFilePrompt', type: 'string', typeOptions: { rows: 4, }, default: 'Analyze these documents and provide a comprehensive summary.', required: true, displayOptions: { show: { operation: ['multiFile'], }, }, description: 'The prompt for processing multiple files', }, // AI Tool Mode fields { displayName: 'Prompt', name: 'aiToolPrompt', type: 'string', typeOptions: { rows: 4, }, default: '', required: true, displayOptions: { show: { operation: ['aiToolMode'], }, }, description: 'The prompt to send to GPT-5. Can reference files if provided.', }, { displayName: 'Include File', name: 'aiToolIncludeFile', type: 'boolean', default: false, displayOptions: { show: { operation: ['aiToolMode'], }, }, description: 'Whether to include a file (PDF, image, etc.) with the request', }, { displayName: 'File Binary Property', name: 'aiToolBinaryProperty', type: 'string', default: 'data', required: true, displayOptions: { show: { operation: ['aiToolMode'], aiToolIncludeFile: [true], }, }, description: 'Name of the binary property containing the file', }, // Upload & Process operation fields { displayName: 'Input Type', name: 'inputType', type: 'options', displayOptions: { show: { operation: ['uploadAndProcess'], }, }, options: [ { name: 'Binary Data', value: 'binary', description: 'Use binary data from previous node (e.g., from S3 or Read Binary File)', }, { name: 'File URL', value: 'url', description: 'Download file from URL (Google Drive, S3 signed URL, etc.)', }, { name: 'File Path', value: 'filePath', description: 'Specify a file path on the server', }, ], default: 'binary', }, { displayName: 'Binary Property', name: 'binaryProperty', type: 'string', default: 'data', required: true, displayOptions: { show: { operation: ['uploadAndProcess'], inputType: ['binary'], }, }, description: 'Name of the binary property containing the PDF file', }, { displayName: 'File URL', name: 'fileUrl', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['uploadAndProcess'], inputType: ['url'], }, }, placeholder: 'https://example.com/file.pdf', description: 'URL to download the file from (supports direct download links, signed URLs, etc.)', }, { displayName: 'File Path', name: 'filePath', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['uploadAndProcess'], inputType: ['filePath'], }, }, placeholder: '/path/to/file.pdf', description: 'Path to the PDF file on the server', }, // Process with File ID fields { displayName: 'File ID', name: 'fileId', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['processFileId'], }, }, placeholder: 'file_abc123...', // eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-id, n8n-nodes-base/node-param-description-miscased-json description: 'The file ID returned from a previous upload (e.g., {{ $json.id }})', }, // Common fields for both operations { displayName: 'Prompt', name: 'prompt', type: 'string', typeOptions: { rows: 4, }, default: 'Analyze this document and provide a summary.', required: true, displayOptions: { show: { operation: ['uploadAndProcess', 'processFileId', 'chat', 'imageAnalysis', 'multiFile'], }, }, description: 'The prompt to send to GPT-5 for processing the file', }, { displayName: 'Additional Options', name: 'additionalOptions', type: 'collection', placeholder: 'Add Option', default: {}, displayOptions: { show: { operation: ['uploadAndProcess', 'processFileId', 'chat', 'imageAnalysis', 'multiFile'], }, }, // eslint-disable-next-line n8n-nodes-base/node-param-collection-type-unsorted-items options: [ { displayName: 'Additional Files', name: 'additionalFiles', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'files', displayName: 'Files', values: [ { displayName: 'File Source', name: 'fileSource', type: 'options', options: [ { name: 'File ID', value: 'fileId', }, { name: 'URL', value: 'url', }, ], default: 'url', }, { displayName: 'File ID', name: 'fileId', type: 'string', displayOptions: { show: { fileSource: ['fileId'], }, }, default: '', placeholder: 'file_img_...', }, { displayName: 'File URL', name: 'url', type: 'string', displayOptions: { show: { fileSource: ['url'], }, }, default: '', placeholder: 'https://example.com/file.pdf', }, ], }, ], description: 'Additional files (PDFs, images, etc.) to include in the request', }, { displayName: 'PDF Files', name: 'pdfFiles', type: 'json', default: '', placeholder: '"file_abc123" or ["file_1", "file_2"] or {{ $json.pdfIds }}', description: 'PDF file IDs to include. Accepts single ID, array, or expression.', }, { displayName: 'Image Files', name: 'imageFiles', type: 'json', default: '', placeholder: '"https://url.jpg" or ["url1", "url2"] or {{ $json.images }}', description: 'Image URLs or file IDs. Accepts single value, array, or expression.', }, { displayName: 'Process All Binary Items', name: 'processAllBinaryItems', type: 'boolean', default: false, description: 'Whether to automatically process all binary data from input items as additional files', }, { displayName: 'Max Tokens', name: 'maxTokens', type: 'number', default: 4096, description: 'Maximum number of tokens to generate', }, { displayName: 'Model', name: 'model', type: 'options', options: [ { name: 'GPT-4.1', value: 'gpt-4.1', description: 'GPT-4.1 model', }, { name: 'GPT-5', value: 'gpt-5', description: 'Most capable GPT-5 model', }, { name: 'GPT-5 Mini', value: 'gpt-5-mini', description: 'Smaller, faster GPT-5 variant', }, { name: 'GPT-5 Nano', value: 'gpt-5-nano', description: 'Smallest, fastest GPT-5 variant', }, { name: 'O3', value: 'o3', description: 'Advanced reasoning model', }, { name: 'O3 Mini', value: 'o3-mini', description: 'Smaller, faster O3 variant', }, { name: 'O3 Pro', value: 'o3-pro', description: 'Enhanced O3 with better reasoning', }, { name: 'O4 Mini', value: 'o4-mini', description: 'Latest mini reasoning model', }, ], default: 'gpt-5', description: 'The OpenAI model to use', }, { displayName: 'Purpose', name: 'purpose', type: 'string', default: 'user_data', description: 'Purpose for file upload (required by OpenAI)', }, { displayName: 'Reasoning Effort', name: 'reasoningEffort', type: 'options', options: [ { name: 'Low', value: 'low', description: 'Lower reasoning effort for faster responses', }, { name: 'Medium', value: 'medium', description: 'Balanced reasoning and speed (default)', }, { name: 'High', value: 'high', description: 'Maximum reasoning capability', }, { name: 'Minimal', value: 'minimal', description: 'Minimal reasoning tokens for fastest responses (GPT-5 only)', }, ], default: 'medium', description: 'Reasoning effort level for enhanced model reasoning', displayOptions: { show: { model: ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'o3', 'o3-pro', 'o3-mini', 'o4-mini'], }, }, }, { displayName: 'Reasoning Summary', name: 'reasoningSummary', type: 'options', options: [ { name: 'None', value: 'none', description: 'No reasoning summary', }, { name: 'Auto', value: 'auto', description: 'Automatic summary generation', }, { name: 'Detailed', value: 'detailed', description: 'Detailed reasoning summary', }, ], default: 'none', description: 'Request summaries of model reasoning (not guaranteed for every request)', displayOptions: { show: { model: ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'o3', 'o3-pro', 'o3-mini'], }, }, }, { displayName: 'Temperature', name: 'temperature', type: 'number', typeOptions: { minValue: 0, maxValue: 2, numberPrecision: 1, }, default: 0.7, description: 'Controls randomness (Not supported by GPT-5 reasoning models - will be ignored)', }, { displayName: 'Verbosity', name: 'verbosity', type: 'options', options: [ { name: 'Low', value: 'low', description: 'Most concise output', }, { name: 'Medium', value: 'medium', description: 'Balanced verbosity', }, { name: 'High', value: 'high', description: 'More detailed output', }, ], default: 'medium', description: 'Control how concise the model output will be (GPT-5 only)', displayOptions: { show: { model: ['gpt-5', 'gpt-5-mini', 'gpt-5-nano'], }, }, }, ], }, ], }; } async execute() { const items = this.getInputData(); const returnData = []; const credentials = await this.getCredentials('openAiGpt5Api'); const baseUrl = credentials.baseUrl || 'https://api.openai.com'; const operation = this.getNodeParameter('operation', 0); for (let i = 0; i < items.length; i++) { try { let fileId; let prompt; let additionalOptions; // Handle AI Tool Mode if (operation === 'aiToolMode') { prompt = this.getNodeParameter('aiToolPrompt', i); additionalOptions = { model: 'gpt-5', reasoningEffort: 'medium', verbosity: 'medium', }; const includeFile = this.getNodeParameter('aiToolIncludeFile', i); if (includeFile) { const binaryProperty = this.getNodeParameter('aiToolBinaryProperty', i); const binaryData = this.helpers.assertBinaryData(i, binaryProperty); const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty); const fileName = binaryData.fileName || 'document'; // Upload file to OpenAI // eslint-disable-next-line @typescript-eslint/no-var-requires const FormData = require('form-data'); const formData = new FormData(); formData.append('file', fileBuffer, { filename: fileName, contentType: binaryData.mimeType || 'application/octet-stream', }); formData.append('purpose', 'user_data'); const uploadOptions = { method: 'POST', url: `${baseUrl}/v1/files`, headers: { 'Authorization': `Bearer ${credentials.apiKey}`, ...formData.getHeaders(), }, body: formData, }; if (credentials.organizationId) { uploadOptions.headers['OpenAI-Organization'] = credentials.organizationId; } const uploadResponse = await this.helpers.httpRequest(uploadOptions); fileId = uploadResponse.id; } } else { // Original operation handling prompt = this.getNodeParameter('prompt', i); additionalOptions = this.getNodeParameter('additionalOptions', i); } // Step 1: Handle file upload if needed if (operation === 'uploadAndProcess') { const inputType = this.getNodeParameter('inputType', i); let fileBuffer; let fileName; if (inputType === 'binary') { const binaryProperty = this.getNodeParameter('binaryProperty', i); const binaryData = this.helpers.assertBinaryData(i, binaryProperty); fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty); fileName = binaryData.fileName || 'document.pdf'; } else if (inputType === 'url') { // Download file from URL const fileUrl = this.getNodeParameter('fileUrl', i); const downloadResponse = await this.helpers.httpRequest({ method: 'GET', url: fileUrl, encoding: 'arraybuffer', returnFullResponse: true, }); fileBuffer = Buffer.from(downloadResponse.body); // Try to extract filename from URL or Content-Disposition header const urlParts = fileUrl.split('/'); fileName = urlParts[urlParts.length - 1].split('?')[0] || 'document.pdf'; if (downloadResponse.headers && downloadResponse.headers['content-disposition']) { const match = downloadResponse.headers['content-disposition'].match(/filename="?([^";\s]+)"?/i); if (match) { fileName = match[1]; } } } else { // File path - would need to read the file const filePath = this.getNodeParameter('filePath', i); // eslint-disable-next-line @typescript-eslint/no-var-requires const fs = require('fs').promises; fileBuffer = await fs.readFile(filePath); fileName = filePath.split('/').pop() || 'document.pdf'; } // Create multipart form data // eslint-disable-next-line @typescript-eslint/no-var-requires const FormData = require('form-data'); const formData = new FormData(); formData.append('file', fileBuffer, { filename: fileName, contentType: 'application/pdf', }); formData.append('purpose', additionalOptions.purpose || 'user_data'); // Upload file to OpenAI const uploadOptions = { method: 'POST', url: `${baseUrl}/v1/files`, headers: { 'Authorization': `Bearer ${credentials.apiKey}`, ...formData.getHeaders(), }, body: formData, }; if (credentials.organizationId) { uploadOptions.headers['OpenAI-Organization'] = credentials.organizationId; } const uploadResponse = await this.helpers.httpRequest(uploadOptions); fileId = uploadResponse.id; } else { // Use provided file ID fileId = this.getNodeParameter('fileId', i); } // Step 2: Call Responses API const model = additionalOptions.model || 'gpt-5'; const requestBody = { model, }; // First, collect all additional files to determine if we need array structure const additionalFileContents = []; // Process traditional additional files if (additionalOptions.additionalFiles) { const files = additionalOptions.additionalFiles.files; if (files && files.length > 0) { for (const file of files) { if (file.fileSource === 'fileId') { // Determine type based on file ID or default to input_file for PDFs // Since we don't have MIME type info here, we'll use input_file for file IDs // and input_image for URLs (which are typically images) additionalFileContents.push({ type: 'input_file', file_id: file.fileId, }); } else if (file.fileSource === 'url') { additionalFileContents.push({ type: 'input_image', image: { url: file.url, }, }); } } } } // Helper function to parse file inputs (single value, array, or comma-separated) const parseFileInput = (input) => { if (!input) return []; // Already an array if (Array.isArray(input)) { return input.map(item => String(item).trim()).filter(item => item); } // String input if (typeof input === 'string') { const trimmed = input.trim(); if (!trimmed) return []; // Check if it's comma-separated if (trimmed.includes(',')) { return trimmed.split(',').map(s => s.trim()).filter(s => s); } // Single item return [trimmed]; } // Other types - try to convert to string const str = String(input).trim(); return str ? [str] : []; }; // Process PDF Files const pdfFiles = parseFileInput(additionalOptions.pdfFiles); for (const pdfFileId of pdfFiles) { if (pdfFileId.startsWith('file_')) { // It's a file ID for a PDF additionalFileContents.push({ type: 'input_file', file_id: pdfFileId, }); } else { console.warn(`PDF file must be a file ID starting with 'file_', got: ${pdfFileId}`); } } // Process Image Files const imageFiles = parseFileInput(additionalOptions.imageFiles); for (const imageRef of imageFiles) { if (imageRef.startsWith('file_')) { // It's a file ID for an image additionalFileContents.push({ type: 'input_image', file_id: imageRef, }); } else if (imageRef.includes('http://') || imageRef.includes('https://')) { // It's a URL - extract clean URL if needed let url = imageRef; const urlMatch = imageRef.match(/(https?:\/\/[^\s\]}"']+)/); if (urlMatch) { url = urlMatch[1]; } additionalFileContents.push({ type: 'input_image', image: { url: url, }, }); } else { console.warn(`Image must be a URL or file ID, got: ${imageRef}`); } } // Process all binary items if enabled if (additionalOptions.processAllBinaryItems) { // Upload each binary item and add to content for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { const item = items[itemIndex]; if (item.binary) { // Process each binary property in the item for (const binaryPropertyName of Object.keys(item.binary)) { try { const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName); const fileBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName); const fileName = binaryData.fileName || `file_${itemIndex}_${binaryPropertyName}`; // Upload file to OpenAI // eslint-disable-next-line @typescript-eslint/no-var-requires const FormData = require('form-data'); const formData = new FormData(); formData.append('file', fileBuffer, { filename: fileName, contentType: binaryData.mimeType || 'application/octet-stream', }); formData.append('purpose', additionalOptions.purpose || 'user_data'); const uploadOptions = { method: 'POST', url: `${baseUrl}/v1/files`, headers: { 'Authorization': `Bearer ${credentials.apiKey}`, ...formData.getHeaders(), }, body: formData, }; if (credentials.organizationId) { uploadOptions.headers['OpenAI-Organization'] = credentials.organizationId; } const uploadResponse = await this.helpers.httpRequest(uploadOptions); // Determine file type based on MIME type const mimeType = binaryData.mimeType || ''; const isImage = mimeType.startsWith('image/'); additionalFileContents.push({ type: isImage ? 'input_image' : 'input_file', file_id: uploadResponse.id, }); } catch (error) { // Continue processing other files if one fails console.error(`Failed to process binary item ${itemIndex}.${binaryPropertyName}:`, error); } } } } } // Now determine input structure based on whether we have ANY files const hasFiles = fileId || additionalFileContents.length > 0; if (hasFiles) { // Use array structure when we have files const contentArray = [ { type: 'input_text', text: prompt, }, ]; // Add primary file if present if (fileId) { // Primary file is typically a PDF (uploaded file) contentArray.push({ type: 'input_file', file_id: fileId, }); } // Add all additional files contentArray.push(...additionalFileContents); requestBody.input = [ { role: 'user', content: contentArray, }, ]; } else { // Use simple string input for text-only requests requestBody.input = prompt; } // Add optional parameters if (additionalOptions.maxTokens) { requestBody.max_tokens = additionalOptions.maxTokens; } // Temperature is not supported by GPT-5 reasoning models // Only add it for non-GPT-5 models if (additionalOptions.temperature !== undefined && !String(model).startsWith('gpt-5')) { requestBody.temperature = additionalOptions.temperature; } // Add reasoning and GPT-5 specific features const modelString = String(model); // Build reasoning object if needed const reasoningConfig = {}; if (additionalOptions.reasoningEffort) { reasoningConfig.effort = additionalOptions.reasoningEffort; } if (additionalOptions.reasoningSummary && additionalOptions.reasoningSummary !== 'none') { reasoningConfig.summary = additionalOptions.reasoningSummary; } // Apply reasoning configuration if (Object.keys(reasoningConfig).length > 0) { if (modelString.startsWith('gpt-5')) { // GPT-5 models use nested reasoning structure requestBody.reasoning = reasoningConfig; } else if (modelString.match(/^o[134]/)) { // O-series models - check if they support nested structure if (reasoningConfig.effort) { requestBody.reasoning_effort = reasoningConfig.effort; } if (reasoningConfig.summary) { // Try nested structure for summary requestBody.reasoning = { summary: reasoningConfig.summary }; } } } // Add GPT-5 specific features if (modelString.startsWith('gpt-5')) { // Add verbosity control if (additionalOptions.verbosity) { if (!requestBody.text) { requestBody.text = {}; } requestBody.text.verbosity = additionalOptions.verbosity; } } const responseOptions = { method: 'POST', url: `${baseUrl}/v1/responses`, headers: { 'Authorization': `Bearer ${credentials.apiKey}`, 'Content-Type': 'application/json', }, body: requestBody, json: true, }; if (credentials.organizationId) { responseOptions.headers['OpenAI-Organization'] = credentials.organizationId; } const response = await this.helpers.httpRequest(responseOptions); // Format the response let outputData; // For AI Tool Mode, simplify the output if (operation === 'aiToolMode') { // Extract just the essential content from the response structure // GPT-5 responses have structure: output[0].content[0].text let textContent = ''; if (response.output && response.output[0]) { const output = response.output[0]; if (output.content && output.content[0]) { textContent = output.content[0].text || ''; } } // Fallback to other possible structures if (!textContent) { textContent = response.choices?.[0]?.message?.content || ''; } outputData = { text: textContent, fileId: fileId, model: response.model || model, usage: response.usage, }; } else { // Return full response with all metadata for regular operations outputData = { ...response, fileId: fileId, prompt: prompt, }; } const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(outputData), { itemData: { item: i } }); returnData.push(...executionData); } catch (error) { // Extract meaningful error message let errorMessage = 'Unknown error'; let errorDetails = {}; if (error.response) { // HTTP error response errorMessage = error.response.statusText || `HTTP ${error.response.status}`; if (error.response.body) { errorDetails = error.response.body; if (error.response.body.error) { errorMessage = error.response.body.error.message || errorMessage; } } } else if (error.message) { errorMessage = error.message; } if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray({ error: errorMessage, details: errorDetails, statusCode: error.response?.status, }), { itemData: { item: i } }); returnData.push(...executionData); continue; } const nodeError = new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage, { description: errorDetails.error?.message || undefined,