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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|