@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.
@@ -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 { client_secret: { value: "rt_secret_1", expires_at: 123 } };
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: { model: "gpt-realtime" },
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: { call_id: "call_1", type: "realtime", model: "gpt-realtime" },
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: { model: "gpt-realtime" },
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: { type: "realtime", model: "gpt-realtime" },
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");