n8n-nodes-openai-compatible-chat-trigger 1.0.0
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 +99 -0
- package/dist/icons/openai-compatible.dark.svg +7 -0
- package/dist/icons/openai-compatible.svg +7 -0
- package/dist/nodes/OpenAiCompatible/OpenAiCompatibleResponse.node.d.ts +5 -0
- package/dist/nodes/OpenAiCompatible/OpenAiCompatibleResponse.node.js +207 -0
- package/dist/nodes/OpenAiCompatible/OpenAiCompatibleTrigger.node.d.ts +5 -0
- package/dist/nodes/OpenAiCompatible/OpenAiCompatibleTrigger.node.js +186 -0
- package/dist/nodes/OpenAiCompatible/icons/openai-compatible.dark.svg +7 -0
- package/dist/nodes/OpenAiCompatible/icons/openai-compatible.svg +7 -0
- package/gulpfile.js +6 -0
- package/icons/openai-compatible.dark.svg +7 -0
- package/icons/openai-compatible.svg +7 -0
- package/index.ts +7 -0
- package/nodes/OpenAiCompatible/OpenAiCompatibleResponse.node.ts +215 -0
- package/nodes/OpenAiCompatible/OpenAiCompatibleTrigger.node.ts +198 -0
- package/nodes/OpenAiCompatible/icons/openai-compatible.dark.svg +7 -0
- package/nodes/OpenAiCompatible/icons/openai-compatible.svg +7 -0
- package/package.json +33 -0
- package/tsconfig.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# OpenAI-Compatible Chat Trigger & Tools for n8n
|
|
2
|
+
|
|
3
|
+
A custom n8n community node package that lets you expose your n8n workflows as direct, OpenAI-compatible chatbot backends. Instead of exposing standard Webhook URLs and mapping custom endpoints, this plugin exposes standard OpenAI API endpoints (`/v1/chat/completions`, `/v1/completions`, `/v1/models`) natively, allowing you to connect n8n workflows directly to any external chatbot program or library that supports OpenAI-compatible APIs (such as LibreChat, Chatbox, etc.).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
1. **OpenAI-Compatible Chat Trigger Node:**
|
|
10
|
+
- Registers a wildcard path that catches POST requests for `/v1/chat/completions` and `/v1/completions`.
|
|
11
|
+
- Directly answers GET requests for `/v1/models` without executing the workflow, returning a custom mock models list.
|
|
12
|
+
- Built-in authorization support (Bearer API keys) that immediately responds with a `401 Unauthorized` block to clients on validation failure.
|
|
13
|
+
- Extracts prompt messages, historical chat messages, and the requested model name into easy-to-use variables (`message`, `messages`, `model`) for downstream nodes.
|
|
14
|
+
|
|
15
|
+
2. **OpenAI Completions Response Node:**
|
|
16
|
+
- Automatically packages generated text from your workflow into the standard OpenAI Chat Completion or Text Completion JSON schemas.
|
|
17
|
+
- Exposes parameters to customize response model name, role, finish reason, and optional custom ID or created timestamp.
|
|
18
|
+
- Feeds back cleanly to the Chat Trigger node when set to "When Last Node Finishes".
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### For Local Development & Testing
|
|
26
|
+
|
|
27
|
+
1. Clone or download this repository.
|
|
28
|
+
2. In the folder, compile the TypeScript code and copy assets:
|
|
29
|
+
```bash
|
|
30
|
+
npm run build
|
|
31
|
+
```
|
|
32
|
+
3. Navigate to your local n8n data directory (typically `~/.n8n/` on Linux/macOS or `C:\Users\<Username>\.n8n\` on Windows).
|
|
33
|
+
4. Create the nested directories if they do not exist:
|
|
34
|
+
```bash
|
|
35
|
+
mkdir -p ~/.n8n/custom/node_modules/
|
|
36
|
+
```
|
|
37
|
+
5. Copy or symlink your package directory (`n8n-nodes-openai-compatible-chat-trigger`) into that folder:
|
|
38
|
+
```bash
|
|
39
|
+
# On Linux/macOS
|
|
40
|
+
ln -s /path/to/n8n-nodes-openai-compatible-chat-trigger ~/.n8n/custom/node_modules/n8n-nodes-openai-compatible-chat-trigger
|
|
41
|
+
```
|
|
42
|
+
6. Restart your n8n server. The new nodes will be discoverable in the node selector.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Node Configurations
|
|
47
|
+
|
|
48
|
+
### 1. OpenAI-Compatible Chat Trigger
|
|
49
|
+
|
|
50
|
+
- **Webhook Path Prefix** (`path`): The custom namespace prefix. For example, setting this to `my-bot` creates these active endpoints:
|
|
51
|
+
- `POST /webhook/my-bot/v1/chat/completions` (starts workflow)
|
|
52
|
+
- `POST /webhook/my-bot/v1/completions` (starts workflow)
|
|
53
|
+
- `GET /webhook/my-bot/v1/models` (immediate GET response list)
|
|
54
|
+
- **Response Mode** (`responseMode`): Set to `When Last Node Finishes` (returns output of the final node) or `Using Respond to Webhook Node`.
|
|
55
|
+
- **Authentication** (`authentication`): `None` or `Header (Bearer Token)`. If set to header authentication, provide an `API Key`.
|
|
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
|
+
|
|
58
|
+
### 2. OpenAI Completions Response
|
|
59
|
+
|
|
60
|
+
- **Response Type** (`responseType`): Choose `Chat Completions` or `Text Completions`.
|
|
61
|
+
- **Response Text** (`text`): The final text content to return to the caller (e.g. `{{ $json.text }}`).
|
|
62
|
+
- **Model Name** (`model`): The model name value injected into the JSON metadata (e.g. `gpt-3.5-turbo`).
|
|
63
|
+
- **Message Role** (`role`): Message role (e.g., `assistant`).
|
|
64
|
+
- **Finish Reason** (`finishReason`): Standard OpenAI stop reasons (e.g., `stop`, `length`, `content_filter`).
|
|
65
|
+
- **Additional Fields**: Optional custom ID and Unix epoch created timestamp overrides.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## API Testing Examples
|
|
71
|
+
|
|
72
|
+
Ensure you use your **Test URL** when testing inside the n8n editor, and the **Production URL** when the workflow is published and activated.
|
|
73
|
+
|
|
74
|
+
### 1. Get Models List
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
curl -X GET "http://localhost:5678/webhook/openai-compatible/v1/models" \
|
|
78
|
+
-H "Authorization: Bearer <your-api-key>"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Send Chat Completions Request
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
curl -X POST "http://localhost:5678/webhook/openai-compatible/v1/chat/completions" \
|
|
85
|
+
-H "Authorization: Bearer <your-api-key>" \
|
|
86
|
+
-H "Content-Type: application/json" \
|
|
87
|
+
-d '{
|
|
88
|
+
"model": "gpt-4o",
|
|
89
|
+
"messages": [
|
|
90
|
+
{"role": "user", "content": "Hello chatbot, how are you?"}
|
|
91
|
+
]
|
|
92
|
+
}'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
This community node package is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect width="24" height="24" rx="6" fill="#1A1F2C"/>
|
|
3
|
+
<path d="M12 4C7.58 4 4 7.13 4 11c0 2.2 1.15 4.18 3 5.46V19l2.7-1.62c.73.17 1.49.26 2.3.26 4.42 0 8-3.13 8-7s-3.58-7-8-7z" fill="none" stroke="#E2E8F0" stroke-width="2" stroke-linejoin="round"/>
|
|
4
|
+
<circle cx="9" cy="11" r="1.5" fill="#6366F1"/>
|
|
5
|
+
<circle cx="12" cy="11" r="1.5" fill="#22D3EE"/>
|
|
6
|
+
<circle cx="15" cy="11" r="1.5" fill="#34D399"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect width="24" height="24" rx="6" fill="#F2F4F7"/>
|
|
3
|
+
<path d="M12 4C7.58 4 4 7.13 4 11c0 2.2 1.15 4.18 3 5.46V19l2.7-1.62c.73.17 1.49.26 2.3.26 4.42 0 8-3.13 8-7s-3.58-7-8-7z" fill="none" stroke="#2D3748" stroke-width="2" stroke-linejoin="round"/>
|
|
4
|
+
<circle cx="9" cy="11" r="1.5" fill="#4F46E5"/>
|
|
5
|
+
<circle cx="12" cy="11" r="1.5" fill="#06B6D4"/>
|
|
6
|
+
<circle cx="15" cy="11" r="1.5" fill="#10B981"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { INodeType, INodeTypeDescription, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
2
|
+
export declare class OpenAiCompatibleResponse implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAiCompatibleResponse = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
class OpenAiCompatibleResponse {
|
|
6
|
+
description = {
|
|
7
|
+
displayName: 'OpenAI Completions Response',
|
|
8
|
+
name: 'openAiCompatibleResponse',
|
|
9
|
+
icon: {
|
|
10
|
+
light: 'file:icons/openai-compatible.svg',
|
|
11
|
+
dark: 'file:icons/openai-compatible.dark.svg',
|
|
12
|
+
},
|
|
13
|
+
group: ['transform'],
|
|
14
|
+
version: 1,
|
|
15
|
+
description: 'Formats response text into standard OpenAI-compatible completions format',
|
|
16
|
+
defaults: {
|
|
17
|
+
name: 'OpenAI Completions Response',
|
|
18
|
+
},
|
|
19
|
+
inputs: ['main'],
|
|
20
|
+
outputs: ['main'],
|
|
21
|
+
properties: [
|
|
22
|
+
{
|
|
23
|
+
displayName: 'Response Type',
|
|
24
|
+
name: 'responseType',
|
|
25
|
+
type: 'options',
|
|
26
|
+
options: [
|
|
27
|
+
{
|
|
28
|
+
name: 'Chat Completions (/v1/chat/completions)',
|
|
29
|
+
value: 'chatCompletions',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Text Completions (/v1/completions)',
|
|
33
|
+
value: 'textCompletions',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
default: 'chatCompletions',
|
|
37
|
+
description: 'The OpenAI-compatible format to use for the response',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
displayName: 'Response Text',
|
|
41
|
+
name: 'text',
|
|
42
|
+
type: 'string',
|
|
43
|
+
typeOptions: {
|
|
44
|
+
rows: 4,
|
|
45
|
+
},
|
|
46
|
+
default: '',
|
|
47
|
+
required: true,
|
|
48
|
+
description: 'The generated text message to send back to the chat program',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
displayName: 'Model Name',
|
|
52
|
+
name: 'model',
|
|
53
|
+
type: 'string',
|
|
54
|
+
default: 'n8n-bot',
|
|
55
|
+
description: 'The model name to include in the OpenAI response JSON',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
displayName: 'Message Role',
|
|
59
|
+
name: 'role',
|
|
60
|
+
type: 'options',
|
|
61
|
+
options: [
|
|
62
|
+
{
|
|
63
|
+
name: 'Assistant',
|
|
64
|
+
value: 'assistant',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'System',
|
|
68
|
+
value: 'system',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'User',
|
|
72
|
+
value: 'user',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
default: 'assistant',
|
|
76
|
+
displayOptions: {
|
|
77
|
+
show: {
|
|
78
|
+
responseType: ['chatCompletions'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
description: 'The role of the message author',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
displayName: 'Finish Reason',
|
|
85
|
+
name: 'finishReason',
|
|
86
|
+
type: 'options',
|
|
87
|
+
options: [
|
|
88
|
+
{
|
|
89
|
+
name: 'Stop',
|
|
90
|
+
value: 'stop',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'Length',
|
|
94
|
+
value: 'length',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'Content Filter',
|
|
98
|
+
value: 'content_filter',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
default: 'stop',
|
|
102
|
+
description: 'The reason the model stopped generating tokens',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
displayName: 'Additional Fields',
|
|
106
|
+
name: 'additionalFields',
|
|
107
|
+
type: 'collection',
|
|
108
|
+
placeholder: 'Add Field',
|
|
109
|
+
default: {},
|
|
110
|
+
options: [
|
|
111
|
+
{
|
|
112
|
+
displayName: 'Custom ID',
|
|
113
|
+
name: 'customId',
|
|
114
|
+
type: 'string',
|
|
115
|
+
default: '',
|
|
116
|
+
description: 'Override the auto-generated response ID',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
displayName: 'Custom Created Timestamp',
|
|
120
|
+
name: 'customCreated',
|
|
121
|
+
type: 'number',
|
|
122
|
+
default: 0,
|
|
123
|
+
description: 'Override the auto-generated creation timestamp (Unix epoch in seconds)',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
async execute() {
|
|
130
|
+
const items = this.getInputData();
|
|
131
|
+
const returnData = [];
|
|
132
|
+
for (let i = 0; i < items.length; i++) {
|
|
133
|
+
try {
|
|
134
|
+
const responseType = this.getNodeParameter('responseType', i);
|
|
135
|
+
const text = this.getNodeParameter('text', i);
|
|
136
|
+
const model = this.getNodeParameter('model', i);
|
|
137
|
+
const role = this.getNodeParameter('role', i, 'assistant');
|
|
138
|
+
const finishReason = this.getNodeParameter('finishReason', i, 'stop');
|
|
139
|
+
const additionalFields = this.getNodeParameter('additionalFields', i, {});
|
|
140
|
+
const id = additionalFields.customId || (responseType === 'chatCompletions' ? `chatcmpl-${(0, crypto_1.randomUUID)()}` : `cmpl-${(0, crypto_1.randomUUID)()}`);
|
|
141
|
+
const created = additionalFields.customCreated || Math.floor(Date.now() / 1000);
|
|
142
|
+
let formattedResponse = {};
|
|
143
|
+
if (responseType === 'chatCompletions') {
|
|
144
|
+
formattedResponse = {
|
|
145
|
+
id,
|
|
146
|
+
object: 'chat.completion',
|
|
147
|
+
created,
|
|
148
|
+
model,
|
|
149
|
+
choices: [
|
|
150
|
+
{
|
|
151
|
+
index: 0,
|
|
152
|
+
message: {
|
|
153
|
+
role,
|
|
154
|
+
content: text,
|
|
155
|
+
},
|
|
156
|
+
finish_reason: finishReason,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
usage: {
|
|
160
|
+
prompt_tokens: 0,
|
|
161
|
+
completion_tokens: 0,
|
|
162
|
+
total_tokens: 0,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
formattedResponse = {
|
|
168
|
+
id,
|
|
169
|
+
object: 'text_completion',
|
|
170
|
+
created,
|
|
171
|
+
model,
|
|
172
|
+
choices: [
|
|
173
|
+
{
|
|
174
|
+
text,
|
|
175
|
+
index: 0,
|
|
176
|
+
logprobs: null,
|
|
177
|
+
finish_reason: finishReason,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
usage: {
|
|
181
|
+
prompt_tokens: 0,
|
|
182
|
+
completion_tokens: 0,
|
|
183
|
+
total_tokens: 0,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
returnData.push({
|
|
188
|
+
json: formattedResponse,
|
|
189
|
+
pairedItem: { item: i },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (this.continueOnFail()) {
|
|
194
|
+
returnData.push({
|
|
195
|
+
json: { error: error.message },
|
|
196
|
+
pairedItem: { item: i },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return [returnData];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
exports.OpenAiCompatibleResponse = OpenAiCompatibleResponse;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { INodeType, INodeTypeDescription, IWebhookFunctions, IWebhookResponseData } from 'n8n-workflow';
|
|
2
|
+
export declare class OpenAiCompatibleTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAiCompatibleTrigger = void 0;
|
|
4
|
+
class OpenAiCompatibleTrigger {
|
|
5
|
+
description = {
|
|
6
|
+
displayName: 'OpenAI-Compatible Chat Trigger',
|
|
7
|
+
name: 'openAiCompatibleTrigger',
|
|
8
|
+
icon: {
|
|
9
|
+
light: 'file:icons/openai-compatible.svg',
|
|
10
|
+
dark: 'file:icons/openai-compatible.dark.svg',
|
|
11
|
+
},
|
|
12
|
+
group: ['trigger'],
|
|
13
|
+
version: 1,
|
|
14
|
+
description: 'Starts workflow on OpenAI-compatible API requests (e.g. /v1/chat/completions)',
|
|
15
|
+
defaults: {
|
|
16
|
+
name: 'OpenAI-Compatible Chat Trigger',
|
|
17
|
+
},
|
|
18
|
+
inputs: [],
|
|
19
|
+
outputs: ['main'],
|
|
20
|
+
webhooks: [
|
|
21
|
+
{
|
|
22
|
+
name: 'default',
|
|
23
|
+
httpMethod: 'POST',
|
|
24
|
+
responseMode: '={{$parameter["responseMode"]}}',
|
|
25
|
+
path: '={{$parameter["path"]}}/*',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'setup',
|
|
29
|
+
httpMethod: 'GET',
|
|
30
|
+
responseMode: 'onReceived',
|
|
31
|
+
path: '={{$parameter["path"]}}/*',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
properties: [
|
|
35
|
+
{
|
|
36
|
+
displayName: 'Webhook Path Prefix',
|
|
37
|
+
name: 'path',
|
|
38
|
+
type: 'string',
|
|
39
|
+
default: 'openai-compatible',
|
|
40
|
+
placeholder: 'openai-compatible',
|
|
41
|
+
required: true,
|
|
42
|
+
description: 'The prefix for the webhook path. The endpoints /v1/chat/completions, /v1/completions, and /v1/models will be registered under this prefix.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
displayName: 'Response Mode',
|
|
46
|
+
name: 'responseMode',
|
|
47
|
+
type: 'options',
|
|
48
|
+
options: [
|
|
49
|
+
{
|
|
50
|
+
name: 'When Last Node Finishes',
|
|
51
|
+
value: 'lastNode',
|
|
52
|
+
description: 'Returns data of the last-executed node',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Using Respond to Webhook Node',
|
|
56
|
+
value: 'responseNode',
|
|
57
|
+
description: 'Defines response in Respond to Webhook node',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
default: 'lastNode',
|
|
61
|
+
description: 'How to respond to the HTTP request for completions',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
displayName: 'Authentication',
|
|
65
|
+
name: 'authentication',
|
|
66
|
+
type: 'options',
|
|
67
|
+
options: [
|
|
68
|
+
{
|
|
69
|
+
name: 'None',
|
|
70
|
+
value: 'none',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Header (Bearer Token)',
|
|
74
|
+
value: 'headerAuth',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
default: 'none',
|
|
78
|
+
description: 'Whether to require authorization header',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
displayName: 'API Key',
|
|
82
|
+
name: 'apiKey',
|
|
83
|
+
type: 'string',
|
|
84
|
+
typeOptions: {
|
|
85
|
+
password: true,
|
|
86
|
+
},
|
|
87
|
+
default: '',
|
|
88
|
+
displayOptions: {
|
|
89
|
+
show: {
|
|
90
|
+
authentication: ['headerAuth'],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
required: true,
|
|
94
|
+
description: 'The Bearer API key that clients must provide in the Authorization header',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
displayName: 'Mock Models',
|
|
98
|
+
name: 'mockModels',
|
|
99
|
+
type: 'string',
|
|
100
|
+
default: 'gpt-3.5-turbo, gpt-4o, n8n-bot',
|
|
101
|
+
description: 'Comma-separated list of models to return from the /v1/models endpoint',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
async webhook() {
|
|
106
|
+
const req = this.getRequestObject();
|
|
107
|
+
const url = req.originalUrl || req.url || '';
|
|
108
|
+
const method = req.method || 'POST';
|
|
109
|
+
// 1. Verify Authorization Header if required
|
|
110
|
+
const authentication = this.getNodeParameter('authentication', 'none');
|
|
111
|
+
if (authentication === 'headerAuth') {
|
|
112
|
+
const authHeader = req.headers['authorization'];
|
|
113
|
+
const expectedApiKey = this.getNodeParameter('apiKey', '');
|
|
114
|
+
if (!authHeader || authHeader !== `Bearer ${expectedApiKey}`) {
|
|
115
|
+
return {
|
|
116
|
+
webhookResponse: {
|
|
117
|
+
statusCode: 401,
|
|
118
|
+
body: {
|
|
119
|
+
error: {
|
|
120
|
+
message: 'Unauthorized: Invalid API key',
|
|
121
|
+
type: 'invalid_request_error',
|
|
122
|
+
code: 'invalid_api_key',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 2. Handle /v1/models endpoint (GET)
|
|
130
|
+
if (method === 'GET' || url.endsWith('/v1/models') || url.endsWith('/models') || url.includes('/models')) {
|
|
131
|
+
const mockModelsStr = this.getNodeParameter('mockModels', 'gpt-3.5-turbo, gpt-4o, n8n-bot');
|
|
132
|
+
const modelsList = mockModelsStr.split(',').map((m) => m.trim()).filter((m) => m !== '');
|
|
133
|
+
const responseData = {
|
|
134
|
+
object: 'list',
|
|
135
|
+
data: modelsList.map((modelId) => ({
|
|
136
|
+
id: modelId,
|
|
137
|
+
object: 'model',
|
|
138
|
+
created: Math.floor(Date.now() / 1000),
|
|
139
|
+
owned_by: 'n8n',
|
|
140
|
+
})),
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
webhookResponse: {
|
|
144
|
+
body: responseData,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// 3. Process chat completions or completions (POST)
|
|
149
|
+
const body = this.getBodyData();
|
|
150
|
+
const query = this.getQueryData();
|
|
151
|
+
const headers = this.getHeaderData();
|
|
152
|
+
let messageContent = '';
|
|
153
|
+
let messagesArray = [];
|
|
154
|
+
let modelName = '';
|
|
155
|
+
let webhookType = 'chatCompletions';
|
|
156
|
+
const isCompletions = url.endsWith('/v1/completions') || url.endsWith('/completions');
|
|
157
|
+
if (isCompletions) {
|
|
158
|
+
messageContent = (body.prompt || '');
|
|
159
|
+
modelName = (body.model || '');
|
|
160
|
+
webhookType = 'completions';
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// default to chatCompletions
|
|
164
|
+
messagesArray = (body.messages || []);
|
|
165
|
+
if (messagesArray.length > 0) {
|
|
166
|
+
const lastMsg = messagesArray[messagesArray.length - 1];
|
|
167
|
+
messageContent = lastMsg.content || '';
|
|
168
|
+
}
|
|
169
|
+
modelName = (body.model || '');
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
workflowData: [
|
|
173
|
+
this.helpers.returnJsonArray({
|
|
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
|
+
}),
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.OpenAiCompatibleTrigger = OpenAiCompatibleTrigger;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect width="24" height="24" rx="6" fill="#1A1F2C"/>
|
|
3
|
+
<path d="M12 4C7.58 4 4 7.13 4 11c0 2.2 1.15 4.18 3 5.46V19l2.7-1.62c.73.17 1.49.26 2.3.26 4.42 0 8-3.13 8-7s-3.58-7-8-7z" fill="none" stroke="#E2E8F0" stroke-width="2" stroke-linejoin="round"/>
|
|
4
|
+
<circle cx="9" cy="11" r="1.5" fill="#6366F1"/>
|
|
5
|
+
<circle cx="12" cy="11" r="1.5" fill="#22D3EE"/>
|
|
6
|
+
<circle cx="15" cy="11" r="1.5" fill="#34D399"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect width="24" height="24" rx="6" fill="#F2F4F7"/>
|
|
3
|
+
<path d="M12 4C7.58 4 4 7.13 4 11c0 2.2 1.15 4.18 3 5.46V19l2.7-1.62c.73.17 1.49.26 2.3.26 4.42 0 8-3.13 8-7s-3.58-7-8-7z" fill="none" stroke="#2D3748" stroke-width="2" stroke-linejoin="round"/>
|
|
4
|
+
<circle cx="9" cy="11" r="1.5" fill="#4F46E5"/>
|
|
5
|
+
<circle cx="12" cy="11" r="1.5" fill="#06B6D4"/>
|
|
6
|
+
<circle cx="15" cy="11" r="1.5" fill="#10B981"/>
|
|
7
|
+
</svg>
|
package/gulpfile.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect width="24" height="24" rx="6" fill="#1A1F2C"/>
|
|
3
|
+
<path d="M12 4C7.58 4 4 7.13 4 11c0 2.2 1.15 4.18 3 5.46V19l2.7-1.62c.73.17 1.49.26 2.3.26 4.42 0 8-3.13 8-7s-3.58-7-8-7z" fill="none" stroke="#E2E8F0" stroke-width="2" stroke-linejoin="round"/>
|
|
4
|
+
<circle cx="9" cy="11" r="1.5" fill="#6366F1"/>
|
|
5
|
+
<circle cx="12" cy="11" r="1.5" fill="#22D3EE"/>
|
|
6
|
+
<circle cx="15" cy="11" r="1.5" fill="#34D399"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect width="24" height="24" rx="6" fill="#F2F4F7"/>
|
|
3
|
+
<path d="M12 4C7.58 4 4 7.13 4 11c0 2.2 1.15 4.18 3 5.46V19l2.7-1.62c.73.17 1.49.26 2.3.26 4.42 0 8-3.13 8-7s-3.58-7-8-7z" fill="none" stroke="#2D3748" stroke-width="2" stroke-linejoin="round"/>
|
|
4
|
+
<circle cx="9" cy="11" r="1.5" fill="#4F46E5"/>
|
|
5
|
+
<circle cx="12" cy="11" r="1.5" fill="#06B6D4"/>
|
|
6
|
+
<circle cx="15" cy="11" r="1.5" fill="#10B981"/>
|
|
7
|
+
</svg>
|
package/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { OpenAiCompatibleTrigger } from './nodes/OpenAiCompatible/OpenAiCompatibleTrigger.node';
|
|
2
|
+
import { OpenAiCompatibleResponse } from './nodes/OpenAiCompatible/OpenAiCompatibleResponse.node';
|
|
3
|
+
|
|
4
|
+
export const nodes = [
|
|
5
|
+
OpenAiCompatibleTrigger,
|
|
6
|
+
OpenAiCompatibleResponse,
|
|
7
|
+
];
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
INodeType,
|
|
4
|
+
INodeTypeDescription,
|
|
5
|
+
IExecuteFunctions,
|
|
6
|
+
INodeExecutionData,
|
|
7
|
+
IDataObject,
|
|
8
|
+
} from 'n8n-workflow';
|
|
9
|
+
|
|
10
|
+
export class OpenAiCompatibleResponse implements INodeType {
|
|
11
|
+
description: INodeTypeDescription = {
|
|
12
|
+
displayName: 'OpenAI Completions Response',
|
|
13
|
+
name: 'openAiCompatibleResponse',
|
|
14
|
+
icon: {
|
|
15
|
+
light: 'file:icons/openai-compatible.svg',
|
|
16
|
+
dark: 'file:icons/openai-compatible.dark.svg',
|
|
17
|
+
},
|
|
18
|
+
group: ['transform'],
|
|
19
|
+
version: 1,
|
|
20
|
+
description: 'Formats response text into standard OpenAI-compatible completions format',
|
|
21
|
+
defaults: {
|
|
22
|
+
name: 'OpenAI Completions Response',
|
|
23
|
+
},
|
|
24
|
+
inputs: ['main'],
|
|
25
|
+
outputs: ['main'],
|
|
26
|
+
properties: [
|
|
27
|
+
{
|
|
28
|
+
displayName: 'Response Type',
|
|
29
|
+
name: 'responseType',
|
|
30
|
+
type: 'options',
|
|
31
|
+
options: [
|
|
32
|
+
{
|
|
33
|
+
name: 'Chat Completions (/v1/chat/completions)',
|
|
34
|
+
value: 'chatCompletions',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Text Completions (/v1/completions)',
|
|
38
|
+
value: 'textCompletions',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
default: 'chatCompletions',
|
|
42
|
+
description: 'The OpenAI-compatible format to use for the response',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
displayName: 'Response Text',
|
|
46
|
+
name: 'text',
|
|
47
|
+
type: 'string',
|
|
48
|
+
typeOptions: {
|
|
49
|
+
rows: 4,
|
|
50
|
+
},
|
|
51
|
+
default: '',
|
|
52
|
+
required: true,
|
|
53
|
+
description: 'The generated text message to send back to the chat program',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
displayName: 'Model Name',
|
|
57
|
+
name: 'model',
|
|
58
|
+
type: 'string',
|
|
59
|
+
default: 'n8n-bot',
|
|
60
|
+
description: 'The model name to include in the OpenAI response JSON',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
displayName: 'Message Role',
|
|
64
|
+
name: 'role',
|
|
65
|
+
type: 'options',
|
|
66
|
+
options: [
|
|
67
|
+
{
|
|
68
|
+
name: 'Assistant',
|
|
69
|
+
value: 'assistant',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'System',
|
|
73
|
+
value: 'system',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'User',
|
|
77
|
+
value: 'user',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
default: 'assistant',
|
|
81
|
+
displayOptions: {
|
|
82
|
+
show: {
|
|
83
|
+
responseType: ['chatCompletions'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
description: 'The role of the message author',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
displayName: 'Finish Reason',
|
|
90
|
+
name: 'finishReason',
|
|
91
|
+
type: 'options',
|
|
92
|
+
options: [
|
|
93
|
+
{
|
|
94
|
+
name: 'Stop',
|
|
95
|
+
value: 'stop',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'Length',
|
|
99
|
+
value: 'length',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'Content Filter',
|
|
103
|
+
value: 'content_filter',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
default: 'stop',
|
|
107
|
+
description: 'The reason the model stopped generating tokens',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
displayName: 'Additional Fields',
|
|
111
|
+
name: 'additionalFields',
|
|
112
|
+
type: 'collection',
|
|
113
|
+
placeholder: 'Add Field',
|
|
114
|
+
default: {},
|
|
115
|
+
options: [
|
|
116
|
+
{
|
|
117
|
+
displayName: 'Custom ID',
|
|
118
|
+
name: 'customId',
|
|
119
|
+
type: 'string',
|
|
120
|
+
default: '',
|
|
121
|
+
description: 'Override the auto-generated response ID',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
displayName: 'Custom Created Timestamp',
|
|
125
|
+
name: 'customCreated',
|
|
126
|
+
type: 'number',
|
|
127
|
+
default: 0,
|
|
128
|
+
description: 'Override the auto-generated creation timestamp (Unix epoch in seconds)',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
136
|
+
const items = this.getInputData();
|
|
137
|
+
const returnData: INodeExecutionData[] = [];
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < items.length; i++) {
|
|
140
|
+
try {
|
|
141
|
+
const responseType = this.getNodeParameter('responseType', i) as string;
|
|
142
|
+
const text = this.getNodeParameter('text', i) as string;
|
|
143
|
+
const model = this.getNodeParameter('model', i) as string;
|
|
144
|
+
const role = this.getNodeParameter('role', i, 'assistant') as string;
|
|
145
|
+
const finishReason = this.getNodeParameter('finishReason', i, 'stop') as string;
|
|
146
|
+
const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject;
|
|
147
|
+
|
|
148
|
+
const id = (additionalFields.customId as string) || (responseType === 'chatCompletions' ? `chatcmpl-${randomUUID()}` : `cmpl-${randomUUID()}`);
|
|
149
|
+
const created = (additionalFields.customCreated as number) || Math.floor(Date.now() / 1000);
|
|
150
|
+
|
|
151
|
+
let formattedResponse: IDataObject = {};
|
|
152
|
+
|
|
153
|
+
if (responseType === 'chatCompletions') {
|
|
154
|
+
formattedResponse = {
|
|
155
|
+
id,
|
|
156
|
+
object: 'chat.completion',
|
|
157
|
+
created,
|
|
158
|
+
model,
|
|
159
|
+
choices: [
|
|
160
|
+
{
|
|
161
|
+
index: 0,
|
|
162
|
+
message: {
|
|
163
|
+
role,
|
|
164
|
+
content: text,
|
|
165
|
+
},
|
|
166
|
+
finish_reason: finishReason,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
usage: {
|
|
170
|
+
prompt_tokens: 0,
|
|
171
|
+
completion_tokens: 0,
|
|
172
|
+
total_tokens: 0,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
} else {
|
|
176
|
+
formattedResponse = {
|
|
177
|
+
id,
|
|
178
|
+
object: 'text_completion',
|
|
179
|
+
created,
|
|
180
|
+
model,
|
|
181
|
+
choices: [
|
|
182
|
+
{
|
|
183
|
+
text,
|
|
184
|
+
index: 0,
|
|
185
|
+
logprobs: null,
|
|
186
|
+
finish_reason: finishReason,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
usage: {
|
|
190
|
+
prompt_tokens: 0,
|
|
191
|
+
completion_tokens: 0,
|
|
192
|
+
total_tokens: 0,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
returnData.push({
|
|
198
|
+
json: formattedResponse,
|
|
199
|
+
pairedItem: { item: i },
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (this.continueOnFail()) {
|
|
203
|
+
returnData.push({
|
|
204
|
+
json: { error: (error as any).message },
|
|
205
|
+
pairedItem: { item: i },
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return [returnData];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INodeType,
|
|
3
|
+
INodeTypeDescription,
|
|
4
|
+
IWebhookFunctions,
|
|
5
|
+
IWebhookResponseData,
|
|
6
|
+
IDataObject,
|
|
7
|
+
} from 'n8n-workflow';
|
|
8
|
+
|
|
9
|
+
export class OpenAiCompatibleTrigger implements INodeType {
|
|
10
|
+
description: INodeTypeDescription = {
|
|
11
|
+
displayName: 'OpenAI-Compatible Chat Trigger',
|
|
12
|
+
name: 'openAiCompatibleTrigger',
|
|
13
|
+
icon: {
|
|
14
|
+
light: 'file:icons/openai-compatible.svg',
|
|
15
|
+
dark: 'file:icons/openai-compatible.dark.svg',
|
|
16
|
+
},
|
|
17
|
+
group: ['trigger'],
|
|
18
|
+
version: 1,
|
|
19
|
+
description: 'Starts workflow on OpenAI-compatible API requests (e.g. /v1/chat/completions)',
|
|
20
|
+
defaults: {
|
|
21
|
+
name: 'OpenAI-Compatible Chat Trigger',
|
|
22
|
+
},
|
|
23
|
+
inputs: [],
|
|
24
|
+
outputs: ['main'],
|
|
25
|
+
webhooks: [
|
|
26
|
+
{
|
|
27
|
+
name: 'default',
|
|
28
|
+
httpMethod: 'POST',
|
|
29
|
+
responseMode: '={{$parameter["responseMode"]}}',
|
|
30
|
+
path: '={{$parameter["path"]}}/*',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'setup',
|
|
34
|
+
httpMethod: 'GET',
|
|
35
|
+
responseMode: 'onReceived',
|
|
36
|
+
path: '={{$parameter["path"]}}/*',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
properties: [
|
|
40
|
+
{
|
|
41
|
+
displayName: 'Webhook Path Prefix',
|
|
42
|
+
name: 'path',
|
|
43
|
+
type: 'string',
|
|
44
|
+
default: 'openai-compatible',
|
|
45
|
+
placeholder: 'openai-compatible',
|
|
46
|
+
required: true,
|
|
47
|
+
description: 'The prefix for the webhook path. The endpoints /v1/chat/completions, /v1/completions, and /v1/models will be registered under this prefix.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
displayName: 'Response Mode',
|
|
51
|
+
name: 'responseMode',
|
|
52
|
+
type: 'options',
|
|
53
|
+
options: [
|
|
54
|
+
{
|
|
55
|
+
name: 'When Last Node Finishes',
|
|
56
|
+
value: 'lastNode',
|
|
57
|
+
description: 'Returns data of the last-executed node',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Using Respond to Webhook Node',
|
|
61
|
+
value: 'responseNode',
|
|
62
|
+
description: 'Defines response in Respond to Webhook node',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
default: 'lastNode',
|
|
66
|
+
description: 'How to respond to the HTTP request for completions',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
displayName: 'Authentication',
|
|
70
|
+
name: 'authentication',
|
|
71
|
+
type: 'options',
|
|
72
|
+
options: [
|
|
73
|
+
{
|
|
74
|
+
name: 'None',
|
|
75
|
+
value: 'none',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'Header (Bearer Token)',
|
|
79
|
+
value: 'headerAuth',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
default: 'none',
|
|
83
|
+
description: 'Whether to require authorization header',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
displayName: 'API Key',
|
|
87
|
+
name: 'apiKey',
|
|
88
|
+
type: 'string',
|
|
89
|
+
typeOptions: {
|
|
90
|
+
password: true,
|
|
91
|
+
},
|
|
92
|
+
default: '',
|
|
93
|
+
displayOptions: {
|
|
94
|
+
show: {
|
|
95
|
+
authentication: ['headerAuth'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
required: true,
|
|
99
|
+
description: 'The Bearer API key that clients must provide in the Authorization header',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
displayName: 'Mock Models',
|
|
103
|
+
name: 'mockModels',
|
|
104
|
+
type: 'string',
|
|
105
|
+
default: 'gpt-3.5-turbo, gpt-4o, n8n-bot',
|
|
106
|
+
description: 'Comma-separated list of models to return from the /v1/models endpoint',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
|
112
|
+
const req = this.getRequestObject();
|
|
113
|
+
const url = req.originalUrl || req.url || '';
|
|
114
|
+
const method = req.method || 'POST';
|
|
115
|
+
|
|
116
|
+
// 1. Verify Authorization Header if required
|
|
117
|
+
const authentication = this.getNodeParameter('authentication', 'none') as string;
|
|
118
|
+
if (authentication === 'headerAuth') {
|
|
119
|
+
const authHeader = req.headers['authorization'];
|
|
120
|
+
const expectedApiKey = this.getNodeParameter('apiKey', '') as string;
|
|
121
|
+
if (!authHeader || authHeader !== `Bearer ${expectedApiKey}`) {
|
|
122
|
+
return {
|
|
123
|
+
webhookResponse: {
|
|
124
|
+
statusCode: 401,
|
|
125
|
+
body: {
|
|
126
|
+
error: {
|
|
127
|
+
message: 'Unauthorized: Invalid API key',
|
|
128
|
+
type: 'invalid_request_error',
|
|
129
|
+
code: 'invalid_api_key',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. Handle /v1/models endpoint (GET)
|
|
138
|
+
if (method === 'GET' || url.endsWith('/v1/models') || url.endsWith('/models') || url.includes('/models')) {
|
|
139
|
+
const mockModelsStr = this.getNodeParameter('mockModels', 'gpt-3.5-turbo, gpt-4o, n8n-bot') as string;
|
|
140
|
+
const modelsList = mockModelsStr.split(',').map((m) => m.trim()).filter((m) => m !== '');
|
|
141
|
+
const responseData = {
|
|
142
|
+
object: 'list',
|
|
143
|
+
data: modelsList.map((modelId) => ({
|
|
144
|
+
id: modelId,
|
|
145
|
+
object: 'model',
|
|
146
|
+
created: Math.floor(Date.now() / 1000),
|
|
147
|
+
owned_by: 'n8n',
|
|
148
|
+
})),
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
webhookResponse: {
|
|
152
|
+
body: responseData,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. Process chat completions or completions (POST)
|
|
158
|
+
const body = this.getBodyData() as IDataObject;
|
|
159
|
+
const query = this.getQueryData() as IDataObject;
|
|
160
|
+
const headers = this.getHeaderData() as IDataObject;
|
|
161
|
+
|
|
162
|
+
let messageContent = '';
|
|
163
|
+
let messagesArray: any[] = [];
|
|
164
|
+
let modelName = '';
|
|
165
|
+
let webhookType = 'chatCompletions';
|
|
166
|
+
|
|
167
|
+
const isCompletions = url.endsWith('/v1/completions') || url.endsWith('/completions');
|
|
168
|
+
|
|
169
|
+
if (isCompletions) {
|
|
170
|
+
messageContent = (body.prompt || '') as string;
|
|
171
|
+
modelName = (body.model || '') as string;
|
|
172
|
+
webhookType = 'completions';
|
|
173
|
+
} else {
|
|
174
|
+
// default to chatCompletions
|
|
175
|
+
messagesArray = (body.messages || []) as any[];
|
|
176
|
+
if (messagesArray.length > 0) {
|
|
177
|
+
const lastMsg = messagesArray[messagesArray.length - 1];
|
|
178
|
+
messageContent = lastMsg.content || '';
|
|
179
|
+
}
|
|
180
|
+
modelName = (body.model || '') as string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
workflowData: [
|
|
185
|
+
this.helpers.returnJsonArray({
|
|
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
|
+
}),
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect width="24" height="24" rx="6" fill="#1A1F2C"/>
|
|
3
|
+
<path d="M12 4C7.58 4 4 7.13 4 11c0 2.2 1.15 4.18 3 5.46V19l2.7-1.62c.73.17 1.49.26 2.3.26 4.42 0 8-3.13 8-7s-3.58-7-8-7z" fill="none" stroke="#E2E8F0" stroke-width="2" stroke-linejoin="round"/>
|
|
4
|
+
<circle cx="9" cy="11" r="1.5" fill="#6366F1"/>
|
|
5
|
+
<circle cx="12" cy="11" r="1.5" fill="#22D3EE"/>
|
|
6
|
+
<circle cx="15" cy="11" r="1.5" fill="#34D399"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
|
2
|
+
<rect width="24" height="24" rx="6" fill="#F2F4F7"/>
|
|
3
|
+
<path d="M12 4C7.58 4 4 7.13 4 11c0 2.2 1.15 4.18 3 5.46V19l2.7-1.62c.73.17 1.49.26 2.3.26 4.42 0 8-3.13 8-7s-3.58-7-8-7z" fill="none" stroke="#2D3748" stroke-width="2" stroke-linejoin="round"/>
|
|
4
|
+
<circle cx="9" cy="11" r="1.5" fill="#4F46E5"/>
|
|
5
|
+
<circle cx="12" cy="11" r="1.5" fill="#06B6D4"/>
|
|
6
|
+
<circle cx="15" cy="11" r="1.5" fill="#10B981"/>
|
|
7
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-openai-compatible-chat-trigger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenAI-compatible chat trigger and helper nodes for n8n.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc && gulp copy-icons",
|
|
8
|
+
"prepublishOnly": "npm run build"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"n8n-community-node-package"
|
|
12
|
+
],
|
|
13
|
+
"author": "Dan Even Segler",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/DanEvenSegler/n8n-nodes-openai-compatible-chat-trigger.git"
|
|
18
|
+
},
|
|
19
|
+
"n8n": {
|
|
20
|
+
"n8nNodesApiVersion": 1,
|
|
21
|
+
"nodes": [
|
|
22
|
+
"dist/nodes/OpenAiCompatible/OpenAiCompatibleTrigger.node.js",
|
|
23
|
+
"dist/nodes/OpenAiCompatible/OpenAiCompatibleResponse.node.js"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.11.0",
|
|
28
|
+
"gulp": "^5.0.0",
|
|
29
|
+
"n8n-workflow": "^1.36.0",
|
|
30
|
+
"rimraf": "^5.0.5",
|
|
31
|
+
"typescript": "^5.3.3"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["es2022"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"moduleResolution": "node"
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"nodes/**/*"
|
|
17
|
+
],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"node_modules",
|
|
20
|
+
"dist"
|
|
21
|
+
]
|
|
22
|
+
}
|