UNPKG

n8n-nodes-openai-gpt5

Version:

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

744 lines 36 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: 'Process files with GPT-5', description: 'Process PDFs and images with OpenAI GPT-5', defaults: { name: 'OpenAI GPT-5', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'openAiGpt5Api', required: true, }, ], properties: [ { displayName: 'Prompt', name: 'prompt', type: 'string', typeOptions: { rows: 4, }, default: 'Analyze these files and provide insights.', required: true, description: 'What you want GPT-5 to do with the files', }, { displayName: 'PDF Files', name: 'pdfFiles', type: 'string', default: '', placeholder: 'file_abc123 or ["file_1", "file_2"] or {{ $json.pdfIds }}', description: 'PDF file IDs (upload PDFs first to get IDs). Single ID, array, or expression.', }, { displayName: 'Images', name: 'imageFiles', type: 'string', default: '', placeholder: 'https://url.jpg or ["url1", "url2"] or {{ $json.images }}', description: 'Image URLs or file IDs. Single value, array, or expression.', }, { displayName: 'Options', name: 'options', type: 'collection', placeholder: 'Add Option', default: {}, options: [ { 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: 'Latest GPT-4.1 with 1M context', }, { name: 'GPT-4.1 Mini', value: 'gpt-4.1-mini', description: 'Faster GPT-4.1 variant', }, { name: 'GPT-4.1 Nano', value: 'gpt-4.1-nano', description: 'Fastest, cheapest 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: 'Fastest, most efficient GPT-5', }, { name: 'O3', value: 'o3', description: 'Advanced reasoning model', }, { name: 'O3 Mini', value: 'o3-mini', description: 'Smaller O3 variant', }, ], default: 'gpt-5', description: 'The OpenAI model to use', }, { displayName: 'Quick Response Mode', name: 'quickMode', type: 'boolean', default: false, description: 'Whether to optimize for speed over quality (uses low reasoning effort and medium search context)', }, { displayName: 'Reasoning Effort', name: 'reasoningEffort', type: 'options', options: [ { name: 'Low', value: 'low', description: 'Faster responses', }, { name: 'Medium', value: 'medium', description: 'Balanced (default)', }, { name: 'High', value: 'high', description: 'Maximum reasoning', }, ], default: 'medium', description: 'Reasoning effort level', }, { displayName: 'Reasoning Summary', name: 'reasoningSummary', type: 'options', options: [ { name: 'None', value: 'none', description: 'No reasoning summary', }, { name: 'Auto', value: 'auto', description: 'Let model decide', }, { name: 'Concise', value: 'concise', description: 'Brief reasoning summary', }, { name: 'Detailed', value: 'detailed', description: 'Full reasoning details', }, ], default: 'none', description: 'Include reasoning summary in response', }, { displayName: 'Temperature', name: 'temperature', type: 'number', default: 0.7, typeOptions: { minValue: 0, maxValue: 2, numberStepSize: 0.1, }, description: 'Controls randomness (0=deterministic, 2=creative)', }, { displayName: 'Timeout', name: 'timeout', type: 'number', default: 600, typeOptions: { minValue: 60, maxValue: 1800, }, description: 'Request timeout in seconds (60-1800). Note: n8n global timeout may also apply - set EXECUTIONS_TIMEOUT environment variable for longer executions.', }, ], }, { displayName: 'Web Search', name: 'webSearch', type: 'collection', placeholder: 'Add Web Search Options', default: {}, options: [ { displayName: 'Allowed Domains', name: 'allowedDomains', type: 'string', default: '', placeholder: 'example.com, docs.site.com', description: 'Comma-separated list of domains to restrict search to (max 20)', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Enable Web Search', name: 'enabled', type: 'boolean', default: false, description: 'Whether to allow the model to search the web for current information', }, { displayName: 'Include Sources', name: 'includeSources', type: 'boolean', default: true, description: 'Whether to include the list of all sources searched in the response', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Search Context Size', name: 'searchContextSize', type: 'options', options: [ { name: 'Low', value: 'low', description: 'Least context, fastest response', }, { name: 'Medium', value: 'medium', description: 'Balanced context and latency (default)', }, { name: 'High', value: 'high', description: 'Most comprehensive context, slower response', }, ], default: 'medium', description: 'Amount of context retrieved from web searches', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'User Location', name: 'userLocation', type: 'fixedCollection', default: {}, typeOptions: { multipleValues: false, }, description: 'Approximate location for localized search results', displayOptions: { show: { enabled: [true], }, }, options: [ { name: 'location', displayName: 'Location', values: [ { displayName: 'Country', name: 'country', type: 'string', default: '', placeholder: 'US', description: 'Two-letter ISO country code (e.g., US, GB, AU)', }, { displayName: 'City', name: 'city', type: 'string', default: '', placeholder: 'San Francisco', description: 'City name for localized results', }, { displayName: 'Region', name: 'region', type: 'string', default: '', placeholder: 'California', description: 'State or region name', }, { displayName: 'Timezone', name: 'timezone', type: 'string', default: '', placeholder: 'America/Los_Angeles', description: 'IANA timezone (e.g., America/New_York)', }, ], }, ], }, ], }, ], }; } async execute() { const items = this.getInputData(); const returnData = []; const credentials = await this.getCredentials('openAiGpt5Api'); const baseUrl = credentials.baseUrl || 'https://api.openai.com'; // Helper function to parse file inputs const parseFileInput = (input) => { if (!input) return []; // Already an array (this happens when expressions return arrays) if (Array.isArray(input)) { // Flatten nested arrays and extract strings const flattened = input.flat(); return flattened.map(item => { // Handle objects that might have url or other properties if (typeof item === 'object' && item !== null) { // Check for common properties if (item.url) return String(item.url).trim(); if (item.path) return String(item.path).trim(); if (item.value) return String(item.value).trim(); // Fallback to JSON string return JSON.stringify(item); } return String(item).trim(); }).filter(item => item && item !== '[object Object]'); } // String input if (typeof input === 'string') { const trimmed = input.trim(); if (!trimmed) return []; // Check if it's a JSON array string if (trimmed.startsWith('[') && trimmed.endsWith(']')) { try { const parsed = JSON.parse(trimmed); if (Array.isArray(parsed)) { return parsed.map(item => String(item).trim()).filter(item => item); } } catch (e) { // Not valid JSON, might be malformed array string // Try to extract URLs or file IDs from it const matches = trimmed.match(/(https?:\/\/[^\s,\]]+|file_[a-zA-Z0-9]+)/g); if (matches) { return matches; } } } // Check if it's comma-separated if (trimmed.includes(',')) { return trimmed.split(',').map(s => s.trim()).filter(s => s); } // Single item return [trimmed]; } // Object input (might be from n8n) if (typeof input === 'object' && input !== null) { // Check if it has a property that contains the actual data if (input.data && Array.isArray(input.data)) { return parseFileInput(input.data); } if (input.body && Array.isArray(input.body)) { return parseFileInput(input.body); } // Try to extract meaningful value if (input.url) return [String(input.url).trim()]; if (input.path) return [String(input.path).trim()]; if (input.value) return [String(input.value).trim()]; } // Other types - try to convert to string const str = String(input).trim(); // Avoid returning meaningless strings if (str && str !== '[object Object]' && str !== 'undefined' && str !== 'null') { return [str]; } return []; }; for (let i = 0; i < items.length; i++) { try { const prompt = this.getNodeParameter('prompt', i); const pdfFiles = this.getNodeParameter('pdfFiles', i, ''); const imageFiles = this.getNodeParameter('imageFiles', i, ''); const options = this.getNodeParameter('options', i, {}); const webSearch = this.getNodeParameter('webSearch', i, {}); let model = options.model || 'gpt-5'; const timeout = options.timeout || 600; const quickMode = options.quickMode; // Apply quick mode optimizations let reasoningEffort = options.reasoningEffort; let searchContextSize = webSearch.searchContextSize; if (quickMode) { // Override settings for speed reasoningEffort = 'low'; searchContextSize = 'medium'; // Use faster models when in quick mode if (model === 'gpt-5') { model = 'gpt-5-mini'; } else if (model === 'gpt-4.1') { model = 'gpt-4.1-mini'; } else if (model === 'o3') { model = 'o3-mini'; } } // Build request body const requestBody = { model, }; // Parse file inputs const pdfIds = parseFileInput(pdfFiles); const images = parseFileInput(imageFiles); // Build content array for requests with files const contentArray = []; // Add text first contentArray.push({ type: 'input_text', text: prompt, }); // Add PDF files for (const pdfId of pdfIds) { if (pdfId.startsWith('file_')) { contentArray.push({ type: 'input_file', file_id: pdfId, }); } else if (pdfId.includes('http://') || pdfId.includes('https://')) { // PDF URL contentArray.push({ type: 'input_file', file_url: pdfId, }); } } // Add images - fix the structure based on docs for (const image of images) { if (image.startsWith('file_')) { // File ID contentArray.push({ type: 'input_image', file_id: image, }); } else if (image.includes('http://') || image.includes('https://')) { // Image URL - corrected structure contentArray.push({ type: 'input_image', image_url: image, }); } } // Use array structure if we have files, otherwise simple string if (pdfIds.length > 0 || images.length > 0) { requestBody.input = [ { role: 'user', content: contentArray, }, ]; } else { // Text-only request - just the string requestBody.input = prompt; } // Add optional parameters if (options.maxTokens) { requestBody.max_tokens = options.maxTokens; } if (options.temperature !== undefined) { requestBody.temperature = options.temperature; } // Add reasoning configuration for supported models (GPT-5 and GPT-4.1 support reasoning) const modelStr = String(model); if ((reasoningEffort || options.reasoningSummary) && (modelStr.startsWith('gpt-5') || modelStr.startsWith('gpt-4.1'))) { const reasoning = {}; if (reasoningEffort) { reasoning.effort = reasoningEffort; } if (options.reasoningSummary && options.reasoningSummary !== 'none') { reasoning.summary = options.reasoningSummary; } requestBody.reasoning = reasoning; } // Add web search tool if enabled if (webSearch.enabled === true) { const tools = []; const webSearchTool = { type: 'web_search', }; // Add search context size if specified (use override from quick mode if applicable) const contextSize = searchContextSize || webSearch.searchContextSize; if (contextSize) { webSearchTool.search_context_size = contextSize; } // Add allowed domains filter if specified if (webSearch.allowedDomains && typeof webSearch.allowedDomains === 'string') { const domains = webSearch.allowedDomains .split(',') .map((d) => d.trim()) .filter((d) => d.length > 0); if (domains.length > 0) { webSearchTool.filters = { allowed_domains: domains.slice(0, 20), // Max 20 domains }; } } // Add user location if specified const location = webSearch.userLocation?.location; if (location && (location.country || location.city || location.region || location.timezone)) { const userLocation = { type: 'approximate', }; if (location.country) userLocation.country = location.country; if (location.city) userLocation.city = location.city; if (location.region) userLocation.region = location.region; if (location.timezone) userLocation.timezone = location.timezone; webSearchTool.user_location = userLocation; } tools.push(webSearchTool); requestBody.tools = tools; // Add include parameter for sources if requested if (webSearch.includeSources === true) { requestBody.include = ['web_search_call.action.sources']; } } // Make API request const responseOptions = { method: 'POST', url: `${baseUrl}/v1/responses`, headers: { 'Authorization': `Bearer ${credentials.apiKey}`, 'Content-Type': 'application/json', }, body: requestBody, json: true, timeout: timeout * 1000, // Convert seconds to milliseconds }; if (credentials.organizationId) { responseOptions.headers['OpenAI-Organization'] = credentials.organizationId; } const response = await this.helpers.httpRequest(responseOptions); // Extract text from response - based on actual API docs let textContent = ''; let reasoningSummary = null; let webSearchResults = null; let citations = []; let sources = []; // Primary structure from Responses API docs if (response.output_text) { textContent = response.output_text; } // Alternative structure with output array (web search responses use this) else if (response.output && Array.isArray(response.output)) { for (const output of response.output) { // Handle web search call output if (output.type === 'web_search_call') { webSearchResults = { id: output.id, status: output.status, query: output.action?.query || null, domains: output.action?.domains || [], sources: output.action?.sources || [], }; // Collect sources if present if (output.action?.sources && Array.isArray(output.action.sources)) { sources = output.action.sources; } } // Handle message output else if (output.type === 'message' && output.content && Array.isArray(output.content)) { for (const content of output.content) { if (content.type === 'output_text' && content.text) { textContent = content.text; // Extract citations from annotations if (content.annotations && Array.isArray(content.annotations)) { citations = content.annotations.filter((a) => a.type === 'url_citation').map((c) => ({ url: c.url, title: c.title, startIndex: c.start_index, endIndex: c.end_index, })); } break; } } } // Alternative content structure else if (output.content && Array.isArray(output.content)) { for (const content of output.content) { if (content.type === 'output_text' && content.text) { textContent = content.text; break; } } } } } // Legacy fallback for chat completions else if (response.choices?.[0]?.message?.content) { textContent = response.choices[0].message.content; } // Extract reasoning summary if present if (response.reasoning && response.reasoning.summary) { reasoningSummary = response.reasoning.summary; } // Also check in the summary array structure from docs else if (response.summary && Array.isArray(response.summary)) { for (const summary of response.summary) { if (summary.type === 'summary_text' && summary.text) { reasoningSummary = summary.text; break; } } } const outputData = { text: textContent, model: response.model || model, usage: response.usage, pdfCount: pdfIds.length, imageCount: images.length, timeout: timeout, quickMode: quickMode, }; // Add web search data if present if (webSearchResults) { outputData.webSearch = webSearchResults; } // Add citations if present if (citations.length > 0) { outputData.citations = citations; } // Add sources if requested and present if (webSearch.includeSources === true && sources.length > 0) { outputData.sources = sources; } // Add reasoning summary if it exists if (reasoningSummary) { outputData.reasoningSummary = reasoningSummary; } // Include full response for debugging outputData.fullResponse = response; const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(outputData), { itemData: { item: i } }); returnData.push(...executionData); } catch (error) { // Error handling let errorMessage = 'Unknown error'; let errorDetails = {}; // Handle timeout errors specifically if (error.code === 'ETIMEDOUT' || error.message?.includes('timeout') || error.message?.includes('ETIMEDOUT')) { const currentOptions = this.getNodeParameter('options', i, {}); const currentTimeout = currentOptions.timeout || 600; const currentQuickMode = currentOptions.quickMode; errorMessage = `Request timed out after ${currentTimeout} seconds. For longer operations, increase the timeout setting or use Quick Response Mode. If using complex reasoning or web search, consider setting n8n's EXECUTIONS_TIMEOUT environment variable to a higher value.`; errorDetails = { timeout: currentTimeout, quickModeAvailable: !currentQuickMode, suggestions: [ 'Enable Quick Response Mode for faster processing', 'Increase the Timeout setting in Options', 'Use lower Reasoning Effort (Low instead of High)', 'Set n8n environment variable EXECUTIONS_TIMEOUT for longer executions', 'Split large documents into smaller chunks' ] }; } // Handle other HTTP errors else if (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; } } } // Handle other errors else if (error.message) { errorMessage = error.message; // Check if it's a different kind of timeout if (error.message.includes('exceeded')) { errorMessage += ` Consider increasing timeout settings or using Quick Response Mode.`; } } if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray({ error: errorMessage, details: errorDetails, }), { itemData: { item: i } }); returnData.push(...executionData); continue; } const nodeError = new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage, { description: errorDetails.error?.message || undefined, }); throw nodeError; } } return [returnData]; } } exports.OpenAiGpt5 = OpenAiGpt5; //# sourceMappingURL=OpenAiGpt5.node.js.map