@inductiv/node-red-openai-api 1.103.0 → 6.27.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.
Files changed (69) hide show
  1. package/README.md +213 -86
  2. package/examples/realtime/client-secrets.json +182 -0
  3. package/examples/responses/computer-use.json +142 -0
  4. package/examples/responses/mcp.json +1 -1
  5. package/examples/responses/phase.json +102 -0
  6. package/examples/responses/tool-search.json +107 -0
  7. package/examples/responses/websocket.json +172 -0
  8. package/internals/openai-api-features-v6.23.0-v6.27.0.md +96 -0
  9. package/lib.js +12696 -15003
  10. package/locales/en-US/node.json +50 -1
  11. package/node.html +1723 -1012
  12. package/node.js +204 -54
  13. package/package.json +9 -7
  14. package/src/assistants/help.html +1 -77
  15. package/src/audio/help.html +1 -37
  16. package/src/batch/help.html +3 -17
  17. package/src/chat/help.html +11 -89
  18. package/src/container-files/help.html +1 -27
  19. package/src/containers/help.html +8 -18
  20. package/src/conversations/help.html +135 -0
  21. package/src/conversations/methods.js +73 -0
  22. package/src/conversations/template.html +10 -0
  23. package/src/embeddings/help.html +1 -11
  24. package/src/evals/help.html +249 -0
  25. package/src/evals/methods.js +114 -0
  26. package/src/evals/template.html +14 -0
  27. package/src/files/help.html +4 -17
  28. package/src/fine-tuning/help.html +1 -35
  29. package/src/images/help.html +1 -45
  30. package/src/lib.js +53 -1
  31. package/src/messages/help.html +19 -39
  32. package/src/messages/methods.js +13 -0
  33. package/src/messages/template.html +7 -18
  34. package/src/models/help.html +1 -5
  35. package/src/moderations/help.html +1 -5
  36. package/src/node.html +126 -37
  37. package/src/realtime/help.html +209 -0
  38. package/src/realtime/methods.js +45 -0
  39. package/src/realtime/template.html +7 -0
  40. package/src/responses/help.html +286 -63
  41. package/src/responses/methods.js +234 -16
  42. package/src/responses/template.html +21 -1
  43. package/src/responses/websocket.js +150 -0
  44. package/src/runs/help.html +1 -123
  45. package/src/skills/help.html +183 -0
  46. package/src/skills/methods.js +99 -0
  47. package/src/skills/template.html +13 -0
  48. package/src/threads/help.html +1 -15
  49. package/src/uploads/help.html +1 -21
  50. package/src/vector-store-file-batches/help.html +1 -27
  51. package/src/vector-store-file-batches/methods.js +5 -5
  52. package/src/vector-store-files/help.html +1 -25
  53. package/src/vector-store-files/methods.js +4 -7
  54. package/src/vector-stores/help.html +2 -31
  55. package/src/vector-stores/methods.js +5 -11
  56. package/src/vector-stores/template.html +7 -22
  57. package/src/videos/help.html +113 -0
  58. package/src/videos/methods.js +50 -0
  59. package/src/videos/template.html +8 -0
  60. package/src/webhooks/help.html +61 -0
  61. package/src/webhooks/methods.js +40 -0
  62. package/src/webhooks/template.html +4 -0
  63. package/test/openai-methods-mapping.test.js +1559 -0
  64. package/test/openai-node-auth-routing.test.js +206 -0
  65. package/test/openai-responses-websocket.test.js +472 -0
  66. package/test/service-host-editor-template.test.js +56 -0
  67. package/test/service-host-node.test.js +185 -0
  68. package/test/services.test.js +150 -0
  69. package/test/utils.test.js +78 -0
@@ -1,23 +1,187 @@
1
1
  const OpenAI = require("openai").OpenAI;
2
+ const { ResponsesWebSocket } = require("./websocket.js");
3
+
4
+ function getResponsesWebSocketConnections(node) {
5
+ if (!node._responsesWebSocketConnections) {
6
+ node._responsesWebSocketConnections = new Map();
7
+ }
8
+
9
+ return node._responsesWebSocketConnections;
10
+ }
11
+
12
+ function ensureResponsesWebSocketCleanup(node) {
13
+ if (node._responsesWebSocketCleanupRegistered) {
14
+ return;
15
+ }
16
+
17
+ if (typeof node.registerCleanupHandler !== "function") {
18
+ throw new Error("OpenAI API node does not support cleanup registration");
19
+ }
20
+
21
+ node._responsesWebSocketCleanupRegistered = true;
22
+ node.registerCleanupHandler(async () => {
23
+ const connections = getResponsesWebSocketConnections(node);
24
+ const closeOperations = [];
25
+
26
+ for (const connection of connections.values()) {
27
+ closeOperations.push(
28
+ connection.close({
29
+ code: 1000,
30
+ reason: "Node-RED node closed",
31
+ })
32
+ );
33
+ }
34
+
35
+ await Promise.all(closeOperations);
36
+ connections.clear();
37
+ });
38
+ }
39
+
40
+ function requireConnectionId(payload) {
41
+ if (typeof payload.connection_id !== "string" || payload.connection_id.trim() === "") {
42
+ throw new Error("msg.payload.connection_id must be a non-empty string");
43
+ }
44
+
45
+ return payload.connection_id;
46
+ }
47
+
48
+ function createResponsesWebSocketEventMessage(connectionId, event) {
49
+ return {
50
+ payload: event,
51
+ openai: {
52
+ transport: "responses.websocket",
53
+ direction: "server",
54
+ connection_id: connectionId,
55
+ event_type: event.type,
56
+ },
57
+ };
58
+ }
59
+
60
+ function attachResponsesWebSocketListeners(node, connectionId, connection) {
61
+ connection.on("event", (event) => {
62
+ node.send(createResponsesWebSocketEventMessage(connectionId, event));
63
+ });
64
+
65
+ connection.on("error", (error) => {
66
+ node.error(error);
67
+ });
68
+
69
+ connection.on("close", () => {
70
+ const connections = getResponsesWebSocketConnections(node);
71
+ connections.delete(connectionId);
72
+ });
73
+ }
74
+
75
+ async function connectResponsesWebSocket(parameters) {
76
+ const node = parameters._node;
77
+ const payload = parameters.payload || {};
78
+ const connectionId = requireConnectionId(payload);
79
+ const connections = getResponsesWebSocketConnections(node);
80
+
81
+ if (connections.has(connectionId)) {
82
+ throw new Error(
83
+ `Responses websocket connection '${connectionId}' is already open on this node`
84
+ );
85
+ }
86
+
87
+ ensureResponsesWebSocketCleanup(node);
88
+
89
+ const connection = new ResponsesWebSocket(this.clientParams);
90
+ const connectionDetails = await connection.open();
91
+
92
+ attachResponsesWebSocketListeners(node, connectionId, connection);
93
+ connections.set(connectionId, connection);
94
+
95
+ return {
96
+ object: "response.websocket.connection",
97
+ action: "connect",
98
+ connection_id: connectionId,
99
+ url: connectionDetails.url,
100
+ };
101
+ }
102
+
103
+ async function sendResponsesWebSocketEvent(parameters) {
104
+ const node = parameters._node;
105
+ const payload = parameters.payload || {};
106
+ const connectionId = requireConnectionId(payload);
107
+ const connections = getResponsesWebSocketConnections(node);
108
+ const connection = connections.get(connectionId);
109
+
110
+ if (!connection) {
111
+ throw new Error(
112
+ `Responses websocket connection '${connectionId}' is not open on this node`
113
+ );
114
+ }
115
+
116
+ if (!payload.event || typeof payload.event !== "object" || Array.isArray(payload.event)) {
117
+ throw new Error("msg.payload.event must be an object");
118
+ }
119
+
120
+ if (payload.event.type !== "response.create") {
121
+ throw new Error("msg.payload.event.type must be 'response.create'");
122
+ }
123
+
124
+ connection.send(payload.event);
125
+
126
+ return {
127
+ object: "response.websocket.client_event",
128
+ action: "send",
129
+ connection_id: connectionId,
130
+ event_type: payload.event.type,
131
+ };
132
+ }
133
+
134
+ async function closeResponsesWebSocket(parameters) {
135
+ const node = parameters._node;
136
+ const payload = parameters.payload || {};
137
+ const connectionId = requireConnectionId(payload);
138
+ const connections = getResponsesWebSocketConnections(node);
139
+ const connection = connections.get(connectionId);
140
+
141
+ if (!connection) {
142
+ throw new Error(
143
+ `Responses websocket connection '${connectionId}' is not open on this node`
144
+ );
145
+ }
146
+
147
+ const closeDetails = await connection.close({
148
+ code: payload.code,
149
+ reason: payload.reason,
150
+ });
151
+
152
+ connections.delete(connectionId);
153
+
154
+ return {
155
+ object: "response.websocket.connection",
156
+ action: "close",
157
+ connection_id: connectionId,
158
+ code: closeDetails.code,
159
+ reason: closeDetails.reason,
160
+ };
161
+ }
162
+
163
+ async function streamResponse(parameters, response) {
164
+ const { _node, msg } = parameters;
165
+ _node.status({
166
+ fill: "green",
167
+ shape: "dot",
168
+ text: "OpenaiApi.status.streaming",
169
+ });
170
+ for await (const chunk of response) {
171
+ if (typeof chunk === "object") {
172
+ const newMsg = { ...msg, payload: chunk };
173
+ _node.send(newMsg);
174
+ }
175
+ }
176
+ _node.status({});
177
+ }
2
178
 
3
179
  async function createModelResponse(parameters) {
4
- const { _node, ...params } = parameters;
5
180
  const openai = new OpenAI(this.clientParams);
6
181
  const response = await openai.responses.create(parameters.payload);
7
182
 
8
- if (params.payload.stream) {
9
- _node.status({
10
- fill: "green",
11
- shape: "dot",
12
- text: "OpenaiApi.status.streaming",
13
- });
14
- for await (const chunk of response) {
15
- if (typeof chunk === "object") {
16
- const newMsg = { ...parameters.msg, payload: chunk };
17
- _node.send(newMsg);
18
- }
19
- }
20
- _node.status({});
183
+ if (parameters.payload.stream) {
184
+ await streamResponse(parameters, response);
21
185
  } else {
22
186
  return response;
23
187
  }
@@ -28,13 +192,32 @@ async function getModelResponse(parameters) {
28
192
  const { response_id, ...params } = parameters.payload;
29
193
  const response = await openai.responses.retrieve(response_id, params);
30
194
 
31
- return response;
195
+ if (params.stream) {
196
+ await streamResponse(parameters, response);
197
+ } else {
198
+ return response;
199
+ }
32
200
  }
33
201
 
34
202
  async function deleteModelResponse(parameters) {
35
203
  const openai = new OpenAI(this.clientParams);
36
204
  const { response_id, ...params } = parameters.payload;
37
- const response = await openai.responses.del(response_id, params);
205
+ const response = await openai.responses.delete(response_id, params);
206
+
207
+ return response;
208
+ }
209
+
210
+ async function cancelModelResponse(parameters) {
211
+ const openai = new OpenAI(this.clientParams);
212
+ const { response_id, ...params } = parameters.payload;
213
+ const response = await openai.responses.cancel(response_id, params);
214
+
215
+ return response;
216
+ }
217
+
218
+ async function compactModelResponse(parameters) {
219
+ const openai = new OpenAI(this.clientParams);
220
+ const response = await openai.responses.compact(parameters.payload);
38
221
 
39
222
  return response;
40
223
  }
@@ -47,9 +230,44 @@ async function listInputItems(parameters) {
47
230
  return [...list.data];
48
231
  }
49
232
 
233
+ async function countInputTokens(parameters) {
234
+ const openai = new OpenAI(this.clientParams);
235
+ const response = await openai.responses.inputTokens.count(parameters.payload);
236
+
237
+ return response;
238
+ }
239
+
240
+ async function manageModelResponseWebSocket(parameters) {
241
+ const payload = parameters.payload || {};
242
+
243
+ if (typeof payload.action !== "string" || payload.action.trim() === "") {
244
+ throw new Error(
245
+ "msg.payload.action must be one of 'connect', 'send', or 'close'"
246
+ );
247
+ }
248
+
249
+ if (payload.action === "connect") {
250
+ return connectResponsesWebSocket.call(this, parameters);
251
+ }
252
+
253
+ if (payload.action === "send") {
254
+ return sendResponsesWebSocketEvent.call(this, parameters);
255
+ }
256
+
257
+ if (payload.action === "close") {
258
+ return closeResponsesWebSocket.call(this, parameters);
259
+ }
260
+
261
+ throw new Error("msg.payload.action must be one of 'connect', 'send', or 'close'");
262
+ }
263
+
50
264
  module.exports = {
51
265
  createModelResponse,
52
266
  getModelResponse,
53
267
  deleteModelResponse,
268
+ cancelModelResponse,
269
+ compactModelResponse,
54
270
  listInputItems,
271
+ countInputTokens,
272
+ manageModelResponseWebSocket,
55
273
  };
@@ -14,9 +14,29 @@
14
14
  data-i18n="OpenaiApi.parameters.deleteModelResponse"
15
15
  ></option>
16
16
 
17
+ <option
18
+ value="cancelModelResponse"
19
+ data-i18n="OpenaiApi.parameters.cancelModelResponse"
20
+ ></option>
21
+
22
+ <option
23
+ value="compactModelResponse"
24
+ data-i18n="OpenaiApi.parameters.compactModelResponse"
25
+ ></option>
26
+
17
27
  <option
18
28
  value="listInputItems"
19
29
  data-i18n="OpenaiApi.parameters.listInputItems"
20
30
  ></option>
21
31
 
22
- </optgroup>
32
+ <option
33
+ value="countInputTokens"
34
+ data-i18n="OpenaiApi.parameters.countInputTokens"
35
+ ></option>
36
+
37
+ <option
38
+ value="manageModelResponseWebSocket"
39
+ data-i18n="OpenaiApi.parameters.manageModelResponseWebSocket"
40
+ ></option>
41
+
42
+ </optgroup>
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+
3
+ const EventEmitter = require("node:events");
4
+ const { WebSocket } = require("ws");
5
+
6
+ function buildResponsesWebSocketURL(clientParams) {
7
+ const baseURL = clientParams.baseURL || "https://api.openai.com/v1";
8
+ const path = "/responses";
9
+ const url = new URL(baseURL + (baseURL.endsWith("/") ? path.slice(1) : path));
10
+ const defaultQuery = clientParams.defaultQuery || {};
11
+
12
+ for (const [key, value] of Object.entries(defaultQuery)) {
13
+ if (value !== undefined && value !== null) {
14
+ url.searchParams.set(key, value);
15
+ }
16
+ }
17
+
18
+ if (url.protocol === "https:") {
19
+ url.protocol = "wss:";
20
+ } else if (url.protocol === "http:") {
21
+ url.protocol = "ws:";
22
+ }
23
+
24
+ return url;
25
+ }
26
+
27
+ function buildResponsesWebSocketHeaders(clientParams) {
28
+ const headers = {};
29
+ const defaultHeaders = clientParams.defaultHeaders || {};
30
+ const hasAuthorizationOverride = Object.prototype.hasOwnProperty.call(
31
+ defaultHeaders,
32
+ "Authorization"
33
+ );
34
+
35
+ for (const [headerName, headerValue] of Object.entries(defaultHeaders)) {
36
+ if (headerValue !== undefined && headerValue !== null) {
37
+ headers[headerName] = headerValue;
38
+ }
39
+ }
40
+
41
+ if (!clientParams.defaultQuery && !hasAuthorizationOverride) {
42
+ headers.Authorization = `Bearer ${clientParams.apiKey}`;
43
+ }
44
+
45
+ if (clientParams.organization) {
46
+ headers["OpenAI-Organization"] = clientParams.organization;
47
+ }
48
+
49
+ return headers;
50
+ }
51
+
52
+ function createEventParseError(error) {
53
+ const parseError = new Error("Could not parse Responses websocket event");
54
+ parseError.cause = error;
55
+ return parseError;
56
+ }
57
+
58
+ class ResponsesWebSocket extends EventEmitter {
59
+ constructor(clientParams) {
60
+ super();
61
+ this.clientParams = clientParams;
62
+ this.url = buildResponsesWebSocketURL(clientParams);
63
+ this.headers = buildResponsesWebSocketHeaders(clientParams);
64
+ this.socket = null;
65
+ }
66
+
67
+ async open() {
68
+ if (this.socket) {
69
+ throw new Error("Responses websocket connection is already open");
70
+ }
71
+
72
+ const socket = new WebSocket(this.url, { headers: this.headers });
73
+
74
+ await new Promise((resolve, reject) => {
75
+ socket.once("open", () => {
76
+ this.socket = socket;
77
+ this.attachSocketListeners(socket);
78
+ resolve();
79
+ });
80
+ socket.once("error", reject);
81
+ });
82
+
83
+ return {
84
+ url: this.url.toString(),
85
+ };
86
+ }
87
+
88
+ attachSocketListeners(socket) {
89
+ socket.on("message", (data) => {
90
+ let event;
91
+
92
+ try {
93
+ event = JSON.parse(data.toString());
94
+ } catch (error) {
95
+ this.emit("error", createEventParseError(error));
96
+ return;
97
+ }
98
+
99
+ this.emit("event", event);
100
+ });
101
+
102
+ socket.on("error", (error) => {
103
+ this.emit("error", error);
104
+ });
105
+
106
+ socket.on("close", (code, reason) => {
107
+ if (this.socket === socket) {
108
+ this.socket = null;
109
+ }
110
+
111
+ this.emit("close", {
112
+ code,
113
+ reason: reason.toString(),
114
+ });
115
+ });
116
+ }
117
+
118
+ send(event) {
119
+ if (!this.socket) {
120
+ throw new Error("Responses websocket connection is not open");
121
+ }
122
+
123
+ this.socket.send(JSON.stringify(event));
124
+ }
125
+
126
+ async close(props = {}) {
127
+ if (!this.socket) {
128
+ throw new Error("Responses websocket connection is not open");
129
+ }
130
+
131
+ const socket = this.socket;
132
+ const closeCode = props.code ?? 1000;
133
+ const closeReason = props.reason ?? "OK";
134
+
135
+ await new Promise((resolve, reject) => {
136
+ socket.once("close", resolve);
137
+ socket.once("error", reject);
138
+ socket.close(closeCode, closeReason);
139
+ });
140
+
141
+ return {
142
+ code: closeCode,
143
+ reason: closeReason,
144
+ };
145
+ }
146
+ }
147
+
148
+ module.exports = {
149
+ ResponsesWebSocket,
150
+ };