UNPKG

@inductiv/node-red-openai-api

Version:

Enhance your Node-RED projects with advanced AI capabilities.

292 lines (234 loc) 7.96 kB
const OpenAI = require("openai").OpenAI; const { ResponsesWebSocket } = require("./websocket.js"); function getResponsesWebSocketConnections(node) { if (!node._responsesWebSocketConnections) { node._responsesWebSocketConnections = new Map(); } return node._responsesWebSocketConnections; } function ensureResponsesWebSocketCleanup(node) { if (node._responsesWebSocketCleanupRegistered) { return; } if (typeof node.registerCleanupHandler !== "function") { throw new Error("OpenAI API node does not support cleanup registration"); } node._responsesWebSocketCleanupRegistered = true; node.registerCleanupHandler(async () => { const connections = getResponsesWebSocketConnections(node); const closeOperations = []; for (const connection of connections.values()) { closeOperations.push( connection.close({ code: 1000, reason: "Node-RED node closed", }) ); } await Promise.all(closeOperations); connections.clear(); }); } function requireConnectionId(payload) { if (typeof payload.connection_id !== "string" || payload.connection_id.trim() === "") { throw new Error("msg.payload.connection_id must be a non-empty string"); } return payload.connection_id; } function createResponsesWebSocketEventMessage(connectionId, event) { return { payload: event, openai: { transport: "responses.websocket", direction: "server", connection_id: connectionId, event_type: event.type, }, }; } function attachResponsesWebSocketListeners(node, connectionId, connection) { connection.on("event", (event) => { node.send(createResponsesWebSocketEventMessage(connectionId, event)); }); connection.on("error", (error) => { node.error(error); }); connection.on("close", () => { const connections = getResponsesWebSocketConnections(node); connections.delete(connectionId); }); } async function connectResponsesWebSocket(parameters) { const node = parameters._node; const payload = parameters.payload || {}; const connectionId = requireConnectionId(payload); const connections = getResponsesWebSocketConnections(node); if (connections.has(connectionId)) { throw new Error( `Responses websocket connection '${connectionId}' is already open on this node` ); } ensureResponsesWebSocketCleanup(node); const connection = new ResponsesWebSocket(this.clientParams); const connectionDetails = await connection.open(); attachResponsesWebSocketListeners(node, connectionId, connection); connections.set(connectionId, connection); return { object: "response.websocket.connection", action: "connect", connection_id: connectionId, url: connectionDetails.url, }; } async function sendResponsesWebSocketEvent(parameters) { const node = parameters._node; const payload = parameters.payload || {}; const connectionId = requireConnectionId(payload); const connections = getResponsesWebSocketConnections(node); const connection = connections.get(connectionId); if (!connection) { throw new Error( `Responses websocket connection '${connectionId}' is not open on this node` ); } if (!payload.event || typeof payload.event !== "object" || Array.isArray(payload.event)) { throw new Error("msg.payload.event must be an object"); } if (payload.event.type !== "response.create") { throw new Error("msg.payload.event.type must be 'response.create'"); } connection.send(payload.event); return { object: "response.websocket.client_event", action: "send", connection_id: connectionId, event_type: payload.event.type, }; } async function closeResponsesWebSocket(parameters) { const node = parameters._node; const payload = parameters.payload || {}; const connectionId = requireConnectionId(payload); const connections = getResponsesWebSocketConnections(node); const connection = connections.get(connectionId); if (!connection) { throw new Error( `Responses websocket connection '${connectionId}' is not open on this node` ); } const closeDetails = await connection.close({ code: payload.code, reason: payload.reason, }); connections.delete(connectionId); return { object: "response.websocket.connection", action: "close", connection_id: connectionId, code: closeDetails.code, reason: closeDetails.reason, }; } async function streamResponse(parameters, response) { const { _node, msg } = parameters; _node.status({ fill: "green", shape: "dot", text: "OpenaiApi.status.streaming", }); for await (const chunk of response) { if (typeof chunk === "object") { const newMsg = { ...msg, payload: chunk }; _node.send(newMsg); } } _node.status({}); } async function createModelResponse(parameters) { const openai = new OpenAI(this.clientParams); const response = await openai.responses.create(parameters.payload); if (parameters.payload.stream) { await streamResponse(parameters, response); } else { return response; } } async function parseModelResponse(parameters) { const openai = new OpenAI(this.clientParams); const response = await openai.responses.parse(parameters.payload); return response; } async function getModelResponse(parameters) { const openai = new OpenAI(this.clientParams); const { response_id, ...params } = parameters.payload; const response = await openai.responses.retrieve(response_id, params); if (params.stream) { await streamResponse(parameters, response); } else { return response; } } async function streamModelResponse(parameters) { const openai = new OpenAI(this.clientParams); const response = openai.responses.stream(parameters.payload); await streamResponse(parameters, response); return response.finalResponse(); } async function deleteModelResponse(parameters) { const openai = new OpenAI(this.clientParams); const { response_id, ...params } = parameters.payload; const response = await openai.responses.delete(response_id, params); return response; } async function cancelModelResponse(parameters) { const openai = new OpenAI(this.clientParams); const { response_id, ...params } = parameters.payload; const response = await openai.responses.cancel(response_id, params); return response; } async function compactModelResponse(parameters) { const openai = new OpenAI(this.clientParams); const response = await openai.responses.compact(parameters.payload); return response; } async function listInputItems(parameters) { const openai = new OpenAI(this.clientParams); const { response_id, ...params } = parameters.payload; const list = await openai.responses.inputItems.list(response_id, params); return [...list.data]; } async function countInputTokens(parameters) { const openai = new OpenAI(this.clientParams); const response = await openai.responses.inputTokens.count(parameters.payload); return response; } async function manageModelResponseWebSocket(parameters) { const payload = parameters.payload || {}; if (typeof payload.action !== "string" || payload.action.trim() === "") { throw new Error( "msg.payload.action must be one of 'connect', 'send', or 'close'" ); } if (payload.action === "connect") { return connectResponsesWebSocket.call(this, parameters); } if (payload.action === "send") { return sendResponsesWebSocketEvent.call(this, parameters); } if (payload.action === "close") { return closeResponsesWebSocket.call(this, parameters); } throw new Error("msg.payload.action must be one of 'connect', 'send', or 'close'"); } module.exports = { createModelResponse, parseModelResponse, getModelResponse, streamModelResponse, deleteModelResponse, cancelModelResponse, compactModelResponse, listInputItems, countInputTokens, manageModelResponseWebSocket, };