n8n-nodes-openai-compatible-chat-trigger 1.0.2 → 1.0.5
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.
|
@@ -19,16 +19,22 @@ class OpenAiCompatibleTrigger {
|
|
|
19
19
|
outputs: ['main'],
|
|
20
20
|
webhooks: [
|
|
21
21
|
{
|
|
22
|
-
name: '
|
|
22
|
+
name: 'chatCompletions',
|
|
23
23
|
httpMethod: 'POST',
|
|
24
24
|
responseMode: '={{$parameter["responseMode"]}}',
|
|
25
|
-
path: '={{$parameter["path"]}}
|
|
25
|
+
path: '={{$parameter["path"]}}/v1/chat/completions',
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
|
-
name: '
|
|
28
|
+
name: 'completions',
|
|
29
|
+
httpMethod: 'POST',
|
|
30
|
+
responseMode: '={{$parameter["responseMode"]}}',
|
|
31
|
+
path: '={{$parameter["path"]}}/v1/completions',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'models',
|
|
29
35
|
httpMethod: 'GET',
|
|
30
36
|
responseMode: 'onReceived',
|
|
31
|
-
path: '={{$parameter["path"]}}
|
|
37
|
+
path: '={{$parameter["path"]}}/v1/models',
|
|
32
38
|
},
|
|
33
39
|
],
|
|
34
40
|
properties: [
|
|
@@ -104,8 +110,7 @@ class OpenAiCompatibleTrigger {
|
|
|
104
110
|
};
|
|
105
111
|
async webhook() {
|
|
106
112
|
const req = this.getRequestObject();
|
|
107
|
-
const
|
|
108
|
-
const method = req.method || 'POST';
|
|
113
|
+
const webhookName = this.getWebhookName();
|
|
109
114
|
// 1. Verify Authorization Header if required
|
|
110
115
|
const authentication = this.getNodeParameter('authentication', 'none');
|
|
111
116
|
if (authentication === 'headerAuth') {
|
|
@@ -127,7 +132,7 @@ class OpenAiCompatibleTrigger {
|
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
134
|
// 2. Handle /v1/models endpoint (GET)
|
|
130
|
-
if (
|
|
135
|
+
if (webhookName === 'models') {
|
|
131
136
|
const mockModelsStr = this.getNodeParameter('mockModels', 'gpt-3.5-turbo, gpt-4o, n8n-bot');
|
|
132
137
|
const modelsList = mockModelsStr.split(',').map((m) => m.trim()).filter((m) => m !== '');
|
|
133
138
|
const responseData = {
|
|
@@ -141,6 +146,7 @@ class OpenAiCompatibleTrigger {
|
|
|
141
146
|
};
|
|
142
147
|
return {
|
|
143
148
|
webhookResponse: {
|
|
149
|
+
statusCode: 200,
|
|
144
150
|
body: responseData,
|
|
145
151
|
},
|
|
146
152
|
};
|
|
@@ -152,33 +158,71 @@ class OpenAiCompatibleTrigger {
|
|
|
152
158
|
let messageContent = '';
|
|
153
159
|
let messagesArray = [];
|
|
154
160
|
let modelName = '';
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (isCompletions) {
|
|
161
|
+
const binaryData = {};
|
|
162
|
+
if (webhookName === 'completions') {
|
|
158
163
|
messageContent = (body.prompt || '');
|
|
159
164
|
modelName = (body.model || '');
|
|
160
|
-
webhookType = 'completions';
|
|
161
165
|
}
|
|
162
166
|
else {
|
|
163
167
|
// default to chatCompletions
|
|
164
168
|
messagesArray = (body.messages || []);
|
|
165
169
|
if (messagesArray.length > 0) {
|
|
166
170
|
const lastMsg = messagesArray[messagesArray.length - 1];
|
|
167
|
-
|
|
171
|
+
if (typeof lastMsg.content === 'string') {
|
|
172
|
+
messageContent = lastMsg.content;
|
|
173
|
+
}
|
|
174
|
+
else if (Array.isArray(lastMsg.content)) {
|
|
175
|
+
// Handle multimodal prompt content (e.g. containing text parts and image data URLs)
|
|
176
|
+
for (const part of lastMsg.content) {
|
|
177
|
+
if (part.type === 'text') {
|
|
178
|
+
messageContent += part.text || '';
|
|
179
|
+
}
|
|
180
|
+
else if (part.type === 'image_url') {
|
|
181
|
+
const url = part.image_url?.url || '';
|
|
182
|
+
if (url.startsWith('data:')) {
|
|
183
|
+
try {
|
|
184
|
+
const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
|
185
|
+
if (matches && matches.length === 3) {
|
|
186
|
+
const mimeType = matches[1];
|
|
187
|
+
const base64Data = matches[2];
|
|
188
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
189
|
+
const extension = mimeType.split('/')[1] || 'png';
|
|
190
|
+
const fileCount = Object.keys(binaryData).length;
|
|
191
|
+
const propertyName = `data_${fileCount}`;
|
|
192
|
+
const fileName = `chat_image_${Date.now()}_${fileCount}.${extension}`;
|
|
193
|
+
binaryData[propertyName] = await this.helpers.prepareBinaryData(buffer, fileName, mimeType);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
// ignore invalid file formats
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
168
203
|
}
|
|
169
204
|
modelName = (body.model || '');
|
|
170
205
|
}
|
|
206
|
+
const responseItem = {
|
|
207
|
+
chatInput: messageContent,
|
|
208
|
+
sessionId: (body.user || body.sessionId || body.conversationId || 'default-session'),
|
|
209
|
+
message: messageContent,
|
|
210
|
+
messages: messagesArray.length > 0 ? messagesArray : undefined,
|
|
211
|
+
model: modelName || undefined,
|
|
212
|
+
body: body,
|
|
213
|
+
headers: headers,
|
|
214
|
+
query: query,
|
|
215
|
+
webhookType: webhookName,
|
|
216
|
+
};
|
|
217
|
+
const executionData = {
|
|
218
|
+
json: responseItem,
|
|
219
|
+
};
|
|
220
|
+
if (Object.keys(binaryData).length > 0) {
|
|
221
|
+
executionData.binary = binaryData;
|
|
222
|
+
}
|
|
171
223
|
return {
|
|
172
224
|
workflowData: [
|
|
173
|
-
|
|
174
|
-
message: messageContent,
|
|
175
|
-
messages: messagesArray.length > 0 ? messagesArray : undefined,
|
|
176
|
-
model: modelName || undefined,
|
|
177
|
-
body: body,
|
|
178
|
-
headers: headers,
|
|
179
|
-
query: query,
|
|
180
|
-
webhookType: webhookType,
|
|
181
|
-
}),
|
|
225
|
+
[executionData]
|
|
182
226
|
],
|
|
183
227
|
};
|
|
184
228
|
}
|
|
@@ -24,16 +24,22 @@ export class OpenAiCompatibleTrigger implements INodeType {
|
|
|
24
24
|
outputs: ['main'],
|
|
25
25
|
webhooks: [
|
|
26
26
|
{
|
|
27
|
-
name: '
|
|
27
|
+
name: 'chatCompletions' as any,
|
|
28
28
|
httpMethod: 'POST',
|
|
29
29
|
responseMode: '={{$parameter["responseMode"]}}',
|
|
30
|
-
path: '={{$parameter["path"]}}
|
|
30
|
+
path: '={{$parameter["path"]}}/v1/chat/completions',
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
|
-
name: '
|
|
33
|
+
name: 'completions' as any,
|
|
34
|
+
httpMethod: 'POST',
|
|
35
|
+
responseMode: '={{$parameter["responseMode"]}}',
|
|
36
|
+
path: '={{$parameter["path"]}}/v1/completions',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'models' as any,
|
|
34
40
|
httpMethod: 'GET',
|
|
35
41
|
responseMode: 'onReceived',
|
|
36
|
-
path: '={{$parameter["path"]}}
|
|
42
|
+
path: '={{$parameter["path"]}}/v1/models',
|
|
37
43
|
},
|
|
38
44
|
],
|
|
39
45
|
properties: [
|
|
@@ -110,8 +116,7 @@ export class OpenAiCompatibleTrigger implements INodeType {
|
|
|
110
116
|
|
|
111
117
|
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
|
112
118
|
const req = this.getRequestObject();
|
|
113
|
-
const
|
|
114
|
-
const method = req.method || 'POST';
|
|
119
|
+
const webhookName = this.getWebhookName();
|
|
115
120
|
|
|
116
121
|
// 1. Verify Authorization Header if required
|
|
117
122
|
const authentication = this.getNodeParameter('authentication', 'none') as string;
|
|
@@ -135,7 +140,7 @@ export class OpenAiCompatibleTrigger implements INodeType {
|
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
// 2. Handle /v1/models endpoint (GET)
|
|
138
|
-
if (
|
|
143
|
+
if (webhookName === 'models') {
|
|
139
144
|
const mockModelsStr = this.getNodeParameter('mockModels', 'gpt-3.5-turbo, gpt-4o, n8n-bot') as string;
|
|
140
145
|
const modelsList = mockModelsStr.split(',').map((m) => m.trim()).filter((m) => m !== '');
|
|
141
146
|
const responseData = {
|
|
@@ -149,6 +154,7 @@ export class OpenAiCompatibleTrigger implements INodeType {
|
|
|
149
154
|
};
|
|
150
155
|
return {
|
|
151
156
|
webhookResponse: {
|
|
157
|
+
statusCode: 200,
|
|
152
158
|
body: responseData,
|
|
153
159
|
},
|
|
154
160
|
};
|
|
@@ -162,35 +168,75 @@ export class OpenAiCompatibleTrigger implements INodeType {
|
|
|
162
168
|
let messageContent = '';
|
|
163
169
|
let messagesArray: any[] = [];
|
|
164
170
|
let modelName = '';
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const isCompletions = url.endsWith('/v1/completions') || url.endsWith('/completions');
|
|
171
|
+
const binaryData: any = {};
|
|
168
172
|
|
|
169
|
-
if (
|
|
173
|
+
if (webhookName === 'completions') {
|
|
170
174
|
messageContent = (body.prompt || '') as string;
|
|
171
175
|
modelName = (body.model || '') as string;
|
|
172
|
-
webhookType = 'completions';
|
|
173
176
|
} else {
|
|
174
177
|
// default to chatCompletions
|
|
175
178
|
messagesArray = (body.messages || []) as any[];
|
|
176
179
|
if (messagesArray.length > 0) {
|
|
177
180
|
const lastMsg = messagesArray[messagesArray.length - 1];
|
|
178
|
-
|
|
181
|
+
|
|
182
|
+
if (typeof lastMsg.content === 'string') {
|
|
183
|
+
messageContent = lastMsg.content;
|
|
184
|
+
} else if (Array.isArray(lastMsg.content)) {
|
|
185
|
+
// Handle multimodal prompt content (e.g. containing text parts and image data URLs)
|
|
186
|
+
for (const part of lastMsg.content) {
|
|
187
|
+
if (part.type === 'text') {
|
|
188
|
+
messageContent += part.text || '';
|
|
189
|
+
} else if (part.type === 'image_url') {
|
|
190
|
+
const url = part.image_url?.url || '';
|
|
191
|
+
if (url.startsWith('data:')) {
|
|
192
|
+
try {
|
|
193
|
+
const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
|
194
|
+
if (matches && matches.length === 3) {
|
|
195
|
+
const mimeType = matches[1];
|
|
196
|
+
const base64Data = matches[2];
|
|
197
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
198
|
+
|
|
199
|
+
const extension = mimeType.split('/')[1] || 'png';
|
|
200
|
+
const fileCount = Object.keys(binaryData).length;
|
|
201
|
+
const propertyName = `data_${fileCount}`;
|
|
202
|
+
const fileName = `chat_image_${Date.now()}_${fileCount}.${extension}`;
|
|
203
|
+
|
|
204
|
+
binaryData[propertyName] = await this.helpers.prepareBinaryData(buffer, fileName, mimeType);
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// ignore invalid file formats
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
179
213
|
}
|
|
180
214
|
modelName = (body.model || '') as string;
|
|
181
215
|
}
|
|
182
216
|
|
|
217
|
+
const responseItem: any = {
|
|
218
|
+
chatInput: messageContent,
|
|
219
|
+
sessionId: (body.user || body.sessionId || body.conversationId || 'default-session') as string,
|
|
220
|
+
message: messageContent,
|
|
221
|
+
messages: messagesArray.length > 0 ? messagesArray : undefined,
|
|
222
|
+
model: modelName || undefined,
|
|
223
|
+
body: body,
|
|
224
|
+
headers: headers,
|
|
225
|
+
query: query,
|
|
226
|
+
webhookType: webhookName,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const executionData: any = {
|
|
230
|
+
json: responseItem,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (Object.keys(binaryData).length > 0) {
|
|
234
|
+
executionData.binary = binaryData;
|
|
235
|
+
}
|
|
236
|
+
|
|
183
237
|
return {
|
|
184
238
|
workflowData: [
|
|
185
|
-
|
|
186
|
-
message: messageContent,
|
|
187
|
-
messages: messagesArray.length > 0 ? messagesArray : undefined,
|
|
188
|
-
model: modelName || undefined,
|
|
189
|
-
body: body,
|
|
190
|
-
headers: headers,
|
|
191
|
-
query: query,
|
|
192
|
-
webhookType: webhookType,
|
|
193
|
-
}),
|
|
239
|
+
[executionData]
|
|
194
240
|
],
|
|
195
241
|
};
|
|
196
242
|
|