n8n-nodes-openai-compatible-chat-trigger 1.0.3 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,6 +54,9 @@ A custom n8n community node package that lets you expose your n8n workflows as d
54
54
  - **Response Mode** (`responseMode`): Set to `When Last Node Finishes` (returns output of the final node) or `Using Respond to Webhook Node`.
55
55
  - **Authentication** (`authentication`): `None` or `Header (Bearer Token)`. If set to header authentication, provide an `API Key`.
56
56
  - **Mock Models** (`mockModels`): Comma-separated list of models returned from the `/v1/models` endpoint (e.g. `gpt-3.5-turbo, gpt-4o, my-n8n-agent`).
57
+ - **Support File Uploads / Vision** (`supportFiles`): Enable/disable parsing of base64 image/file payloads from the client message. Parses them into native n8n binary files.
58
+ - **Allowed File Types** (`allowedFileTypes`): Comma-separated list of allowed MIME types (e.g., `image/png, image/jpeg, image/webp`). Non-matching files are skipped.
59
+ - **Support Tools / Function Calling** (`supportTools`): Enable/disable tools. If disabled, incoming tool calling definitions are filtered out to prevent errors in downstream nodes.
57
60
 
58
61
  ### 2. OpenAI Completions Response
59
62
 
@@ -106,6 +106,32 @@ class OpenAiCompatibleTrigger {
106
106
  default: 'gpt-3.5-turbo, gpt-4o, n8n-bot',
107
107
  description: 'Comma-separated list of models to return from the /v1/models endpoint',
108
108
  },
109
+ {
110
+ displayName: 'Support File Uploads / Vision',
111
+ name: 'supportFiles',
112
+ type: 'boolean',
113
+ default: true,
114
+ description: 'Whether to allow and parse base64 image/file uploads from the client',
115
+ },
116
+ {
117
+ displayName: 'Allowed File Types',
118
+ name: 'allowedFileTypes',
119
+ type: 'string',
120
+ default: 'image/png, image/jpeg, image/webp',
121
+ displayOptions: {
122
+ show: {
123
+ supportFiles: [true],
124
+ },
125
+ },
126
+ description: 'Comma-separated list of allowed MIME types (e.g. image/png, image/jpeg). Use * or *.* to allow all.',
127
+ },
128
+ {
129
+ displayName: 'Support Tools / Function Calling',
130
+ name: 'supportTools',
131
+ type: 'boolean',
132
+ default: true,
133
+ description: 'Whether to support tools. If disabled, incoming tool/function definitions will be automatically filtered out to prevent downstream errors.',
134
+ },
109
135
  ],
110
136
  };
111
137
  async webhook() {
@@ -146,6 +172,7 @@ class OpenAiCompatibleTrigger {
146
172
  };
147
173
  return {
148
174
  webhookResponse: {
175
+ statusCode: 200,
149
176
  body: responseData,
150
177
  },
151
178
  };
@@ -154,9 +181,21 @@ class OpenAiCompatibleTrigger {
154
181
  const body = this.getBodyData();
155
182
  const query = this.getQueryData();
156
183
  const headers = this.getHeaderData();
184
+ const supportFiles = this.getNodeParameter('supportFiles', true);
185
+ const allowedFileTypesStr = this.getNodeParameter('allowedFileTypes', 'image/png, image/jpeg, image/webp');
186
+ const supportTools = this.getNodeParameter('supportTools', true);
187
+ // Clean up tool calling if tools are disabled
188
+ if (!supportTools) {
189
+ if (body.tools)
190
+ delete body.tools;
191
+ if (body.tool_choice)
192
+ delete body.tool_choice;
193
+ }
157
194
  let messageContent = '';
158
195
  let messagesArray = [];
159
196
  let modelName = '';
197
+ const binaryData = {};
198
+ const allowedFileTypes = allowedFileTypesStr.split(',').map(t => t.trim().toLowerCase());
160
199
  if (webhookName === 'completions') {
161
200
  messageContent = (body.prompt || '');
162
201
  modelName = (body.model || '');
@@ -166,21 +205,73 @@ class OpenAiCompatibleTrigger {
166
205
  messagesArray = (body.messages || []);
167
206
  if (messagesArray.length > 0) {
168
207
  const lastMsg = messagesArray[messagesArray.length - 1];
169
- messageContent = lastMsg.content || '';
208
+ if (typeof lastMsg.content === 'string') {
209
+ messageContent = lastMsg.content;
210
+ }
211
+ else if (Array.isArray(lastMsg.content)) {
212
+ // Handle multimodal prompt content (e.g. containing text parts and image data URLs)
213
+ for (const part of lastMsg.content) {
214
+ if (part.type === 'text') {
215
+ messageContent += part.text || '';
216
+ }
217
+ else if (part.type === 'image_url' && supportFiles) {
218
+ const url = part.image_url?.url || '';
219
+ if (url.startsWith('data:')) {
220
+ try {
221
+ const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
222
+ if (matches && matches.length === 3) {
223
+ const mimeType = matches[1].toLowerCase();
224
+ // Check if file type is allowed
225
+ const isAllowed = allowedFileTypes.some(type => {
226
+ if (type === '*' || type === '*/*' || type === 'all')
227
+ return true;
228
+ if (type.endsWith('/*')) {
229
+ const prefix = type.split('/')[0];
230
+ return mimeType.startsWith(prefix + '/');
231
+ }
232
+ return mimeType === type;
233
+ });
234
+ if (isAllowed) {
235
+ const base64Data = matches[2];
236
+ const buffer = Buffer.from(base64Data, 'base64');
237
+ const extension = mimeType.split('/')[1] || 'png';
238
+ const fileCount = Object.keys(binaryData).length;
239
+ const propertyName = `data_${fileCount}`;
240
+ const fileName = `chat_image_${Date.now()}_${fileCount}.${extension}`;
241
+ binaryData[propertyName] = await this.helpers.prepareBinaryData(buffer, fileName, mimeType);
242
+ }
243
+ }
244
+ }
245
+ catch (e) {
246
+ // ignore invalid file formats
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
170
252
  }
171
253
  modelName = (body.model || '');
172
254
  }
255
+ const responseItem = {
256
+ chatInput: messageContent,
257
+ sessionId: (body.user || body.sessionId || body.conversationId || 'default-session'),
258
+ message: messageContent,
259
+ messages: messagesArray.length > 0 ? messagesArray : undefined,
260
+ model: modelName || undefined,
261
+ body: body,
262
+ headers: headers,
263
+ query: query,
264
+ webhookType: webhookName,
265
+ };
266
+ const executionData = {
267
+ json: responseItem,
268
+ };
269
+ if (Object.keys(binaryData).length > 0) {
270
+ executionData.binary = binaryData;
271
+ }
173
272
  return {
174
273
  workflowData: [
175
- this.helpers.returnJsonArray({
176
- message: messageContent,
177
- messages: messagesArray.length > 0 ? messagesArray : undefined,
178
- model: modelName || undefined,
179
- body: body,
180
- headers: headers,
181
- query: query,
182
- webhookType: webhookName,
183
- }),
274
+ [executionData]
184
275
  ],
185
276
  };
186
277
  }
@@ -111,6 +111,32 @@ export class OpenAiCompatibleTrigger implements INodeType {
111
111
  default: 'gpt-3.5-turbo, gpt-4o, n8n-bot',
112
112
  description: 'Comma-separated list of models to return from the /v1/models endpoint',
113
113
  },
114
+ {
115
+ displayName: 'Support File Uploads / Vision',
116
+ name: 'supportFiles',
117
+ type: 'boolean',
118
+ default: true,
119
+ description: 'Whether to allow and parse base64 image/file uploads from the client',
120
+ },
121
+ {
122
+ displayName: 'Allowed File Types',
123
+ name: 'allowedFileTypes',
124
+ type: 'string',
125
+ default: 'image/png, image/jpeg, image/webp',
126
+ displayOptions: {
127
+ show: {
128
+ supportFiles: [true],
129
+ },
130
+ },
131
+ description: 'Comma-separated list of allowed MIME types (e.g. image/png, image/jpeg). Use * or *.* to allow all.',
132
+ },
133
+ {
134
+ displayName: 'Support Tools / Function Calling',
135
+ name: 'supportTools',
136
+ type: 'boolean',
137
+ default: true,
138
+ description: 'Whether to support tools. If disabled, incoming tool/function definitions will be automatically filtered out to prevent downstream errors.',
139
+ },
114
140
  ],
115
141
  };
116
142
 
@@ -154,6 +180,7 @@ export class OpenAiCompatibleTrigger implements INodeType {
154
180
  };
155
181
  return {
156
182
  webhookResponse: {
183
+ statusCode: 200,
157
184
  body: responseData,
158
185
  },
159
186
  };
@@ -164,9 +191,22 @@ export class OpenAiCompatibleTrigger implements INodeType {
164
191
  const query = this.getQueryData() as IDataObject;
165
192
  const headers = this.getHeaderData() as IDataObject;
166
193
 
194
+ const supportFiles = this.getNodeParameter('supportFiles', true) as boolean;
195
+ const allowedFileTypesStr = this.getNodeParameter('allowedFileTypes', 'image/png, image/jpeg, image/webp') as string;
196
+ const supportTools = this.getNodeParameter('supportTools', true) as boolean;
197
+
198
+ // Clean up tool calling if tools are disabled
199
+ if (!supportTools) {
200
+ if (body.tools) delete body.tools;
201
+ if (body.tool_choice) delete body.tool_choice;
202
+ }
203
+
167
204
  let messageContent = '';
168
205
  let messagesArray: any[] = [];
169
206
  let modelName = '';
207
+ const binaryData: any = {};
208
+
209
+ const allowedFileTypes = allowedFileTypesStr.split(',').map(t => t.trim().toLowerCase());
170
210
 
171
211
  if (webhookName === 'completions') {
172
212
  messageContent = (body.prompt || '') as string;
@@ -176,22 +216,78 @@ export class OpenAiCompatibleTrigger implements INodeType {
176
216
  messagesArray = (body.messages || []) as any[];
177
217
  if (messagesArray.length > 0) {
178
218
  const lastMsg = messagesArray[messagesArray.length - 1];
179
- messageContent = lastMsg.content || '';
219
+
220
+ if (typeof lastMsg.content === 'string') {
221
+ messageContent = lastMsg.content;
222
+ } else if (Array.isArray(lastMsg.content)) {
223
+ // Handle multimodal prompt content (e.g. containing text parts and image data URLs)
224
+ for (const part of lastMsg.content) {
225
+ if (part.type === 'text') {
226
+ messageContent += part.text || '';
227
+ } else if (part.type === 'image_url' && supportFiles) {
228
+ const url = part.image_url?.url || '';
229
+ if (url.startsWith('data:')) {
230
+ try {
231
+ const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
232
+ if (matches && matches.length === 3) {
233
+ const mimeType = matches[1].toLowerCase();
234
+
235
+ // Check if file type is allowed
236
+ const isAllowed = allowedFileTypes.some(type => {
237
+ if (type === '*' || type === '*/*' || type === 'all') return true;
238
+ if (type.endsWith('/*')) {
239
+ const prefix = type.split('/')[0];
240
+ return mimeType.startsWith(prefix + '/');
241
+ }
242
+ return mimeType === type;
243
+ });
244
+
245
+ if (isAllowed) {
246
+ const base64Data = matches[2];
247
+ const buffer = Buffer.from(base64Data, 'base64');
248
+
249
+ const extension = mimeType.split('/')[1] || 'png';
250
+ const fileCount = Object.keys(binaryData).length;
251
+ const propertyName = `data_${fileCount}`;
252
+ const fileName = `chat_image_${Date.now()}_${fileCount}.${extension}`;
253
+
254
+ binaryData[propertyName] = await this.helpers.prepareBinaryData(buffer, fileName, mimeType);
255
+ }
256
+ }
257
+ } catch (e) {
258
+ // ignore invalid file formats
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
180
264
  }
181
265
  modelName = (body.model || '') as string;
182
266
  }
183
267
 
268
+ const responseItem: any = {
269
+ chatInput: messageContent,
270
+ sessionId: (body.user || body.sessionId || body.conversationId || 'default-session') as string,
271
+ message: messageContent,
272
+ messages: messagesArray.length > 0 ? messagesArray : undefined,
273
+ model: modelName || undefined,
274
+ body: body,
275
+ headers: headers,
276
+ query: query,
277
+ webhookType: webhookName,
278
+ };
279
+
280
+ const executionData: any = {
281
+ json: responseItem,
282
+ };
283
+
284
+ if (Object.keys(binaryData).length > 0) {
285
+ executionData.binary = binaryData;
286
+ }
287
+
184
288
  return {
185
289
  workflowData: [
186
- this.helpers.returnJsonArray({
187
- message: messageContent,
188
- messages: messagesArray.length > 0 ? messagesArray : undefined,
189
- model: modelName || undefined,
190
- body: body,
191
- headers: headers,
192
- query: query,
193
- webhookType: webhookName,
194
- }),
290
+ [executionData]
195
291
  ],
196
292
  };
197
293
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-openai-compatible-chat-trigger",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "OpenAI-compatible chat trigger and helper nodes for n8n.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {