@inductiv/node-red-openai-api 6.22.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.
- package/README.md +141 -84
- package/examples/realtime/client-secrets.json +182 -0
- package/examples/responses/computer-use.json +142 -0
- package/examples/responses/phase.json +102 -0
- package/examples/responses/tool-search.json +107 -0
- package/examples/responses/websocket.json +172 -0
- package/internals/openai-api-features-v6.23.0-v6.27.0.md +96 -0
- package/lib.js +4073 -117
- package/locales/en-US/node.json +1 -0
- package/node.html +177 -11
- package/node.js +10 -0
- package/package.json +4 -3
- package/src/realtime/help.html +89 -9
- package/src/responses/help.html +83 -2
- package/src/responses/methods.js +185 -0
- package/src/responses/template.html +5 -0
- package/src/responses/websocket.js +150 -0
- package/test/openai-methods-mapping.test.js +346 -7
- package/test/openai-node-auth-routing.test.js +3 -0
- package/test/openai-responses-websocket.test.js +472 -0
- package/test/service-host-editor-template.test.js +3 -0
- package/test/service-host-node.test.js +3 -0
- package/test/services.test.js +3 -0
- package/test/utils.test.js +3 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
// This file is the broad API surface sanity check.
|
|
4
|
+
// It proves our method wrappers, examples, and help text still line up with the OpenAI SDK contract.
|
|
5
|
+
|
|
3
6
|
const assert = require("node:assert/strict");
|
|
4
7
|
const fs = require("node:fs");
|
|
5
8
|
const path = require("node:path");
|
|
@@ -158,6 +161,298 @@ test("responses methods map delete/cancel/compact/input-items/input-tokens to Op
|
|
|
158
161
|
]);
|
|
159
162
|
});
|
|
160
163
|
|
|
164
|
+
test("responses create forwards phase, prompt_cache_key, tool_search, defer_loading, computer, and gpt-5.4 payloads", async () => {
|
|
165
|
+
const calls = [];
|
|
166
|
+
|
|
167
|
+
class FakeOpenAI {
|
|
168
|
+
constructor(clientParams) {
|
|
169
|
+
calls.push({ method: "ctor", clientParams });
|
|
170
|
+
this.responses = {
|
|
171
|
+
create: async (payload) => {
|
|
172
|
+
calls.push({ method: "responses.create", payload });
|
|
173
|
+
return { id: "resp_123", status: "completed" };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const requestPayload = {
|
|
180
|
+
model: "gpt-5.4",
|
|
181
|
+
prompt_cache_key: "responses-agentic-demo-v1",
|
|
182
|
+
input: [
|
|
183
|
+
{
|
|
184
|
+
type: "message",
|
|
185
|
+
role: "assistant",
|
|
186
|
+
phase: "commentary",
|
|
187
|
+
content: [{ type: "output_text", text: "Planning the response." }],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: "message",
|
|
191
|
+
role: "user",
|
|
192
|
+
content: [{ type: "input_text", text: "Summarize the release work." }],
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
tools: [
|
|
196
|
+
{ type: "tool_search" },
|
|
197
|
+
{
|
|
198
|
+
type: "function",
|
|
199
|
+
name: "lookup_release_ticket",
|
|
200
|
+
description: "Look up a release ticket by id.",
|
|
201
|
+
parameters: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
ticket_id: { type: "string" },
|
|
205
|
+
},
|
|
206
|
+
required: ["ticket_id"],
|
|
207
|
+
additionalProperties: false,
|
|
208
|
+
},
|
|
209
|
+
strict: true,
|
|
210
|
+
defer_loading: true,
|
|
211
|
+
},
|
|
212
|
+
{ type: "computer" },
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
await withMockedOpenAI(FakeOpenAI, async () => {
|
|
217
|
+
const modulePath = require.resolve("../src/responses/methods.js");
|
|
218
|
+
delete require.cache[modulePath];
|
|
219
|
+
const responsesMethods = require("../src/responses/methods.js");
|
|
220
|
+
|
|
221
|
+
const clientContext = { clientParams: { apiKey: "sk-test", baseURL: "https://api.example.com/v1" } };
|
|
222
|
+
|
|
223
|
+
const response = await responsesMethods.createModelResponse.call(clientContext, {
|
|
224
|
+
payload: requestPayload,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
assert.deepEqual(response, { id: "resp_123", status: "completed" });
|
|
228
|
+
|
|
229
|
+
delete require.cache[modulePath];
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const createCalls = calls.filter((entry) => entry.method === "responses.create");
|
|
233
|
+
assert.deepEqual(createCalls, [
|
|
234
|
+
{
|
|
235
|
+
method: "responses.create",
|
|
236
|
+
payload: requestPayload,
|
|
237
|
+
},
|
|
238
|
+
]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("responses example flows remain valid JSON and cover the documented agentic payload shapes", () => {
|
|
242
|
+
const phaseExamplePath = path.join(
|
|
243
|
+
__dirname,
|
|
244
|
+
"..",
|
|
245
|
+
"examples",
|
|
246
|
+
"responses",
|
|
247
|
+
"phase.json"
|
|
248
|
+
);
|
|
249
|
+
const toolSearchExamplePath = path.join(
|
|
250
|
+
__dirname,
|
|
251
|
+
"..",
|
|
252
|
+
"examples",
|
|
253
|
+
"responses",
|
|
254
|
+
"tool-search.json"
|
|
255
|
+
);
|
|
256
|
+
const computerUseExamplePath = path.join(
|
|
257
|
+
__dirname,
|
|
258
|
+
"..",
|
|
259
|
+
"examples",
|
|
260
|
+
"responses",
|
|
261
|
+
"computer-use.json"
|
|
262
|
+
);
|
|
263
|
+
const websocketExamplePath = path.join(
|
|
264
|
+
__dirname,
|
|
265
|
+
"..",
|
|
266
|
+
"examples",
|
|
267
|
+
"responses",
|
|
268
|
+
"websocket.json"
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const phaseExample = JSON.parse(fs.readFileSync(phaseExamplePath, "utf8"));
|
|
272
|
+
const toolSearchExample = JSON.parse(
|
|
273
|
+
fs.readFileSync(toolSearchExamplePath, "utf8")
|
|
274
|
+
);
|
|
275
|
+
const computerUseExample = JSON.parse(
|
|
276
|
+
fs.readFileSync(computerUseExamplePath, "utf8")
|
|
277
|
+
);
|
|
278
|
+
const websocketExample = JSON.parse(
|
|
279
|
+
fs.readFileSync(websocketExamplePath, "utf8")
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
[phaseExample, toolSearchExample, computerUseExample].forEach((flow) => {
|
|
283
|
+
assert.ok(Array.isArray(flow));
|
|
284
|
+
const openaiNode = flow.find((entry) => entry.type === "OpenAI API");
|
|
285
|
+
const commentNodes = flow.filter((entry) => entry.type === "comment");
|
|
286
|
+
assert.ok(openaiNode);
|
|
287
|
+
assert.equal(openaiNode.method, "createModelResponse");
|
|
288
|
+
assert.ok(commentNodes.length >= 1);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const phaseInjectNode = phaseExample.find(
|
|
292
|
+
(entry) => entry.type === "inject" && entry.name === "Create Phased Response"
|
|
293
|
+
);
|
|
294
|
+
const toolSearchInjectNode = toolSearchExample.find(
|
|
295
|
+
(entry) =>
|
|
296
|
+
entry.type === "inject" && entry.name === "Create Tool Search Request"
|
|
297
|
+
);
|
|
298
|
+
const computerCreateInjectNode = computerUseExample.find(
|
|
299
|
+
(entry) => entry.type === "inject" && entry.name === "Create Computer Request"
|
|
300
|
+
);
|
|
301
|
+
const computerFollowupInjectNode = computerUseExample.find(
|
|
302
|
+
(entry) =>
|
|
303
|
+
entry.type === "inject" &&
|
|
304
|
+
entry.name === "Submit Computer Screenshot (edit placeholders)"
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
assert.ok(phaseInjectNode);
|
|
308
|
+
assert.ok(toolSearchInjectNode);
|
|
309
|
+
assert.ok(computerCreateInjectNode);
|
|
310
|
+
assert.ok(computerFollowupInjectNode);
|
|
311
|
+
|
|
312
|
+
const phaseMessage = JSON.parse(
|
|
313
|
+
phaseInjectNode.props.find((prop) => prop.p === "ai.input[0]").v
|
|
314
|
+
);
|
|
315
|
+
const toolSearchTool = JSON.parse(
|
|
316
|
+
toolSearchInjectNode.props.find((prop) => prop.p === "ai.tools[0]").v
|
|
317
|
+
);
|
|
318
|
+
const deferredMcpTool = JSON.parse(
|
|
319
|
+
toolSearchInjectNode.props.find((prop) => prop.p === "ai.tools[1]").v
|
|
320
|
+
);
|
|
321
|
+
const computerTool = JSON.parse(
|
|
322
|
+
computerCreateInjectNode.props.find((prop) => prop.p === "ai.tools[0]").v
|
|
323
|
+
);
|
|
324
|
+
const computerCallOutput = JSON.parse(
|
|
325
|
+
computerFollowupInjectNode.props.find((prop) => prop.p === "ai.input[0]").v
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
assert.equal(
|
|
329
|
+
phaseInjectNode.props.find((prop) => prop.p === "ai.prompt_cache_key").v,
|
|
330
|
+
"responses-phase-example-v1"
|
|
331
|
+
);
|
|
332
|
+
assert.equal(phaseMessage.phase, "commentary");
|
|
333
|
+
assert.equal(toolSearchTool.type, "tool_search");
|
|
334
|
+
assert.equal(deferredMcpTool.defer_loading, true);
|
|
335
|
+
assert.equal(computerTool.type, "computer");
|
|
336
|
+
assert.equal(computerCallOutput.type, "computer_call_output");
|
|
337
|
+
assert.equal(computerCallOutput.output.type, "computer_screenshot");
|
|
338
|
+
|
|
339
|
+
assert.ok(Array.isArray(websocketExample));
|
|
340
|
+
const websocketOpenaiNode = websocketExample.find(
|
|
341
|
+
(entry) => entry.type === "OpenAI API"
|
|
342
|
+
);
|
|
343
|
+
const websocketCommentNodes = websocketExample.filter(
|
|
344
|
+
(entry) => entry.type === "comment"
|
|
345
|
+
);
|
|
346
|
+
const connectInjectNode = websocketExample.find(
|
|
347
|
+
(entry) =>
|
|
348
|
+
entry.type === "inject" && entry.name === "Connect Responses WebSocket"
|
|
349
|
+
);
|
|
350
|
+
const sendInjectNode = websocketExample.find(
|
|
351
|
+
(entry) =>
|
|
352
|
+
entry.type === "inject" && entry.name === "Send response.create Event"
|
|
353
|
+
);
|
|
354
|
+
const closeInjectNode = websocketExample.find(
|
|
355
|
+
(entry) =>
|
|
356
|
+
entry.type === "inject" && entry.name === "Close Responses WebSocket"
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
assert.ok(websocketOpenaiNode);
|
|
360
|
+
assert.equal(websocketOpenaiNode.method, "manageModelResponseWebSocket");
|
|
361
|
+
assert.ok(websocketCommentNodes.length >= 2);
|
|
362
|
+
assert.ok(connectInjectNode);
|
|
363
|
+
assert.ok(sendInjectNode);
|
|
364
|
+
assert.ok(closeInjectNode);
|
|
365
|
+
assert.equal(
|
|
366
|
+
connectInjectNode.props.find((prop) => prop.p === "ai.action").v,
|
|
367
|
+
"connect"
|
|
368
|
+
);
|
|
369
|
+
assert.equal(
|
|
370
|
+
sendInjectNode.props.find((prop) => prop.p === "ai.action").v,
|
|
371
|
+
"send"
|
|
372
|
+
);
|
|
373
|
+
assert.equal(
|
|
374
|
+
closeInjectNode.props.find((prop) => prop.p === "ai.action").v,
|
|
375
|
+
"close"
|
|
376
|
+
);
|
|
377
|
+
assert.deepEqual(
|
|
378
|
+
JSON.parse(sendInjectNode.props.find((prop) => prop.p === "ai.event").v),
|
|
379
|
+
{
|
|
380
|
+
type: "response.create",
|
|
381
|
+
model: "gpt-5.4",
|
|
382
|
+
input: "Say hello from Responses websocket mode in one sentence.",
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("responses help documents websocket lifecycle contract", () => {
|
|
388
|
+
const responsesHelpPath = path.join(__dirname, "..", "src", "responses", "help.html");
|
|
389
|
+
const responsesHelp = fs.readFileSync(responsesHelpPath, "utf8");
|
|
390
|
+
|
|
391
|
+
assert.match(responsesHelp, /Manage Model Response WebSocket/);
|
|
392
|
+
assert.match(responsesHelp, /msg\.payload\.action/);
|
|
393
|
+
assert.match(responsesHelp, /connect<\/code>, <code>send<\/code>, or <code>close<\/code>/);
|
|
394
|
+
assert.match(responsesHelp, /msg\.openai/);
|
|
395
|
+
assert.match(responsesHelp, /custom auth headers and query-string auth/);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("realtime example flow remains valid JSON and documents the nested session contract", () => {
|
|
399
|
+
const realtimeExamplePath = path.join(
|
|
400
|
+
__dirname,
|
|
401
|
+
"..",
|
|
402
|
+
"examples",
|
|
403
|
+
"realtime",
|
|
404
|
+
"client-secrets.json"
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const realtimeExample = JSON.parse(fs.readFileSync(realtimeExamplePath, "utf8"));
|
|
408
|
+
assert.ok(Array.isArray(realtimeExample));
|
|
409
|
+
|
|
410
|
+
const openaiNode = realtimeExample.find((entry) => entry.type === "OpenAI API");
|
|
411
|
+
const commentNodes = realtimeExample.filter((entry) => entry.type === "comment");
|
|
412
|
+
const explainerComment = realtimeExample.find(
|
|
413
|
+
(entry) => entry.type === "comment" && entry.name === "What is a client secret?"
|
|
414
|
+
);
|
|
415
|
+
const realtimeInjectNode = realtimeExample.find(
|
|
416
|
+
(entry) =>
|
|
417
|
+
entry.type === "inject" &&
|
|
418
|
+
entry.name === "Create Realtime 1.5 Client Secret"
|
|
419
|
+
);
|
|
420
|
+
const audioInjectNode = realtimeExample.find(
|
|
421
|
+
(entry) =>
|
|
422
|
+
entry.type === "inject" &&
|
|
423
|
+
entry.name === "Create Audio 1.5 Client Secret"
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
assert.ok(openaiNode);
|
|
427
|
+
assert.equal(openaiNode.method, "createRealtimeClientSecret");
|
|
428
|
+
assert.ok(commentNodes.length >= 3);
|
|
429
|
+
assert.ok(explainerComment);
|
|
430
|
+
assert.match(explainerComment.info, /not your long-lived OpenAI API key/);
|
|
431
|
+
assert.ok(realtimeInjectNode);
|
|
432
|
+
assert.ok(audioInjectNode);
|
|
433
|
+
|
|
434
|
+
assert.equal(
|
|
435
|
+
realtimeInjectNode.props.find((prop) => prop.p === "ai.session.type").v,
|
|
436
|
+
"realtime"
|
|
437
|
+
);
|
|
438
|
+
assert.equal(
|
|
439
|
+
realtimeInjectNode.props.find((prop) => prop.p === "ai.session.model").v,
|
|
440
|
+
"gpt-realtime-1.5"
|
|
441
|
+
);
|
|
442
|
+
assert.equal(
|
|
443
|
+
audioInjectNode.props.find((prop) => prop.p === "ai.session.type").v,
|
|
444
|
+
"realtime"
|
|
445
|
+
);
|
|
446
|
+
assert.equal(
|
|
447
|
+
audioInjectNode.props.find((prop) => prop.p === "ai.session.model").v,
|
|
448
|
+
"gpt-audio-1.5"
|
|
449
|
+
);
|
|
450
|
+
assert.equal(
|
|
451
|
+
realtimeInjectNode.props.find((prop) => prop.p === "ai.expires_after.seconds").v,
|
|
452
|
+
"600"
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
161
456
|
test("responses retrieve streams chunks when stream=true", async () => {
|
|
162
457
|
const calls = [];
|
|
163
458
|
|
|
@@ -876,7 +1171,7 @@ test("videos methods map to OpenAI SDK videos endpoints", async () => {
|
|
|
876
1171
|
]);
|
|
877
1172
|
});
|
|
878
1173
|
|
|
879
|
-
test("realtime methods map to OpenAI SDK realtime endpoints", async () => {
|
|
1174
|
+
test("realtime methods map to OpenAI SDK realtime endpoints and pass newer model ids through unchanged", async () => {
|
|
880
1175
|
const calls = [];
|
|
881
1176
|
|
|
882
1177
|
class FakeOpenAI {
|
|
@@ -886,7 +1181,11 @@ test("realtime methods map to OpenAI SDK realtime endpoints", async () => {
|
|
|
886
1181
|
clientSecrets: {
|
|
887
1182
|
create: async (body) => {
|
|
888
1183
|
calls.push({ method: "realtime.clientSecrets.create", body });
|
|
889
|
-
return {
|
|
1184
|
+
return {
|
|
1185
|
+
expires_at: 123,
|
|
1186
|
+
session: body.session,
|
|
1187
|
+
value: "ek_rt_secret_1",
|
|
1188
|
+
};
|
|
890
1189
|
},
|
|
891
1190
|
},
|
|
892
1191
|
calls: {
|
|
@@ -914,13 +1213,34 @@ test("realtime methods map to OpenAI SDK realtime endpoints", async () => {
|
|
|
914
1213
|
|
|
915
1214
|
const clientContext = { clientParams: { apiKey: "sk-test" } };
|
|
916
1215
|
|
|
1216
|
+
const clientSecretPayload = {
|
|
1217
|
+
expires_after: {
|
|
1218
|
+
anchor: "created_at",
|
|
1219
|
+
seconds: 600,
|
|
1220
|
+
},
|
|
1221
|
+
session: {
|
|
1222
|
+
type: "realtime",
|
|
1223
|
+
model: "gpt-realtime-1.5",
|
|
1224
|
+
instructions: "Speak clearly and keep responses concise.",
|
|
1225
|
+
output_modalities: ["audio"],
|
|
1226
|
+
},
|
|
1227
|
+
};
|
|
917
1228
|
const secret = await realtimeMethods.createRealtimeClientSecret.call(clientContext, {
|
|
918
|
-
payload:
|
|
1229
|
+
payload: clientSecretPayload,
|
|
1230
|
+
});
|
|
1231
|
+
assert.deepEqual(secret, {
|
|
1232
|
+
expires_at: 123,
|
|
1233
|
+
session: clientSecretPayload.session,
|
|
1234
|
+
value: "ek_rt_secret_1",
|
|
919
1235
|
});
|
|
920
|
-
assert.deepEqual(secret, { client_secret: { value: "rt_secret_1", expires_at: 123 } });
|
|
921
1236
|
|
|
922
1237
|
const accepted = await realtimeMethods.acceptRealtimeCall.call(clientContext, {
|
|
923
|
-
payload: {
|
|
1238
|
+
payload: {
|
|
1239
|
+
call_id: "call_1",
|
|
1240
|
+
type: "realtime",
|
|
1241
|
+
model: "gpt-audio-1.5",
|
|
1242
|
+
output_modalities: ["audio"],
|
|
1243
|
+
},
|
|
924
1244
|
});
|
|
925
1245
|
assert.deepEqual(accepted, { call_id: "call_1", status: "accepted" });
|
|
926
1246
|
|
|
@@ -946,12 +1266,27 @@ test("realtime methods map to OpenAI SDK realtime endpoints", async () => {
|
|
|
946
1266
|
assert.deepEqual(realtimeCalls, [
|
|
947
1267
|
{
|
|
948
1268
|
method: "realtime.clientSecrets.create",
|
|
949
|
-
body: {
|
|
1269
|
+
body: {
|
|
1270
|
+
expires_after: {
|
|
1271
|
+
anchor: "created_at",
|
|
1272
|
+
seconds: 600,
|
|
1273
|
+
},
|
|
1274
|
+
session: {
|
|
1275
|
+
type: "realtime",
|
|
1276
|
+
model: "gpt-realtime-1.5",
|
|
1277
|
+
instructions: "Speak clearly and keep responses concise.",
|
|
1278
|
+
output_modalities: ["audio"],
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
950
1281
|
},
|
|
951
1282
|
{
|
|
952
1283
|
method: "realtime.calls.accept",
|
|
953
1284
|
callId: "call_1",
|
|
954
|
-
body: {
|
|
1285
|
+
body: {
|
|
1286
|
+
type: "realtime",
|
|
1287
|
+
model: "gpt-audio-1.5",
|
|
1288
|
+
output_modalities: ["audio"],
|
|
1289
|
+
},
|
|
955
1290
|
},
|
|
956
1291
|
{
|
|
957
1292
|
method: "realtime.calls.hangup",
|
|
@@ -1176,6 +1511,10 @@ test("editor templates and locale expose latest methods", () => {
|
|
|
1176
1511
|
assert.match(evalsHelp, /⋙ List Eval Run Output Items/);
|
|
1177
1512
|
assert.match(realtimeHelp, /⋙ Create Realtime Client Secret/);
|
|
1178
1513
|
assert.match(realtimeHelp, /⋙ Reject Realtime Call/);
|
|
1514
|
+
assert.match(realtimeHelp, /msg\.payload\.session/);
|
|
1515
|
+
assert.match(realtimeHelp, /session\.model/);
|
|
1516
|
+
assert.match(realtimeHelp, /gpt-realtime-1\.5/);
|
|
1517
|
+
assert.match(realtimeHelp, /gpt-audio-1\.5/);
|
|
1179
1518
|
assert.match(skillsHelp, /⋙ Create Skill/);
|
|
1180
1519
|
assert.match(skillsHelp, /⋙ List Skill Versions/);
|
|
1181
1520
|
assert.match(videosHelp, /⋙ Download Video Content/);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
// This file keeps the Service Host auth rules honest.
|
|
4
|
+
// It checks that header auth, query auth, and default Authorization behavior are all routed the way this node promises.
|
|
5
|
+
|
|
3
6
|
const assert = require("node:assert/strict");
|
|
4
7
|
const EventEmitter = require("node:events");
|
|
5
8
|
const test = require("node:test");
|