UNPKG

nodejs-gpt

Version:

A ChatGPT implementation using the official ChatGPT model via OpenAI's API.

249 lines (219 loc) 8.29 kB
#!/usr/bin/env node import fastify from 'fastify'; import cors from '@fastify/cors'; import { FastifySSEPlugin } from '@waylaidwanderer/fastify-sse-v2'; import fs from 'fs'; import { pathToFileURL } from 'url'; import { KeyvFile } from 'keyv-file'; import ChatGPTClient from '../src/ChatGPTClient.js'; import ChatGPTBrowserClient from '../src/ChatGPTBrowserClient.js'; import BingAIClient from '../src/BingAIClient.js'; const arg = process.argv.find(_arg => _arg.startsWith('--settings')); const path = arg?.split('=')[1] ?? './settings.js'; let settings; if (fs.existsSync(path)) { // get the full path const fullPath = fs.realpathSync(path); settings = (await import(pathToFileURL(fullPath).toString())).default; } else { if (arg) { console.error('Error: the file specified by the --settings parameter does not exist.'); } else { console.error('Error: the settings.js file does not exist.'); } process.exit(1); } if (settings.storageFilePath && !settings.cacheOptions.store) { // make the directory and file if they don't exist const dir = settings.storageFilePath.split('/').slice(0, -1).join('/'); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } if (!fs.existsSync(settings.storageFilePath)) { fs.writeFileSync(settings.storageFilePath, ''); } settings.cacheOptions.store = new KeyvFile({ filename: settings.storageFilePath }); } const clientToUse = settings.apiOptions?.clientToUse || settings.clientToUse || 'chatgpt'; const perMessageClientOptionsWhitelist = settings.apiOptions?.perMessageClientOptionsWhitelist || null; const server = fastify(); await server.register(FastifySSEPlugin); await server.register(cors, { origin: '*', }); server.get('/ping', () => Date.now().toString()); server.post('/conversation', async (request, reply) => { const body = request.body || {}; const abortController = new AbortController(); reply.raw.on('close', () => { if (abortController.signal.aborted === false) { abortController.abort(); } }); let onProgress; if (body.stream === true) { onProgress = (token) => { if (settings.apiOptions?.debug) { console.debug(token); } if (token !== '[DONE]') { reply.sse({ id: '', data: JSON.stringify(token) }); } }; } else { onProgress = null; } let result; let error; try { if (!body.message) { const invalidError = new Error(); invalidError.data = { code: 400, message: 'The message parameter is required.', }; // noinspection ExceptionCaughtLocallyJS throw invalidError; } let clientToUseForMessage = clientToUse; const clientOptions = filterClientOptions(body.clientOptions, clientToUseForMessage); if (clientOptions && clientOptions.clientToUse) { clientToUseForMessage = clientOptions.clientToUse; delete clientOptions.clientToUse; } let { shouldGenerateTitle } = body; if (typeof shouldGenerateTitle !== 'boolean') { shouldGenerateTitle = settings.apiOptions?.generateTitles || false; } const messageClient = getClient(clientToUseForMessage); result = await messageClient.sendMessage(body.message, { jailbreakConversationId: body.jailbreakConversationId, conversationId: body.conversationId ? body.conversationId.toString() : undefined, parentMessageId: body.parentMessageId ? body.parentMessageId.toString() : undefined, systemMessage: body.systemMessage, context: body.context, conversationSignature: body.conversationSignature, clientId: body.clientId, invocationId: body.invocationId, shouldGenerateTitle, // only used for ChatGPTClient toneStyle: body.toneStyle, clientOptions, onProgress, abortController, }); } catch (e) { error = e; } if (result !== undefined) { if (settings.apiOptions?.debug) { console.debug(result); } if (body.stream === true) { reply.sse({ event: 'result', id: '', data: JSON.stringify(result) }); reply.sse({ id: '', data: '[DONE]' }); await nextTick(); return reply.raw.end(); } return reply.send(result); } const code = error?.data?.code || (error.name === 'UnauthorizedRequest' ? 401 : 503); if (code === 503) { console.error(error); } else if (settings.apiOptions?.debug) { console.debug(error); } const message = error?.data?.message || error?.message || `There was an error communicating with ${clientToUse === 'bing' ? 'Bing' : 'ChatGPT'}.`; if (body.stream === true) { reply.sse({ id: '', event: 'error', data: JSON.stringify({ code, error: message, }), }); await nextTick(); return reply.raw.end(); } return reply.code(code).send({ error: message }); }); server.listen({ port: settings.apiOptions?.port || settings.port || 3000, host: settings.apiOptions?.host || 'localhost', }, (error) => { if (error) { console.error(error); process.exit(1); } }); function nextTick() { return new Promise(resolve => setTimeout(resolve, 0)); } function getClient(clientToUseForMessage) { switch (clientToUseForMessage) { case 'bing': return new BingAIClient({ ...settings.bingAiClient, cache: settings.cacheOptions }); case 'chatgpt-browser': return new ChatGPTBrowserClient( settings.chatGptBrowserClient, settings.cacheOptions, ); case 'chatgpt': return new ChatGPTClient( settings.openaiApiKey || settings.chatGptClient.openaiApiKey, settings.chatGptClient, settings.cacheOptions, ); default: throw new Error(`Invalid clientToUse: ${clientToUseForMessage}`); } } /** * Filter objects to only include whitelisted properties set in * `settings.js` > `apiOptions.perMessageClientOptionsWhitelist`. * Returns original object if no whitelist is set. * @param {*} inputOptions * @param clientToUseForMessage */ function filterClientOptions(inputOptions, clientToUseForMessage) { if (!inputOptions || !perMessageClientOptionsWhitelist) { return null; } // If inputOptions.clientToUse is set and is in the whitelist, use it instead of the default if ( perMessageClientOptionsWhitelist.validClientsToUse && inputOptions.clientToUse && perMessageClientOptionsWhitelist.validClientsToUse.includes(inputOptions.clientToUse) ) { clientToUseForMessage = inputOptions.clientToUse; } else { inputOptions.clientToUse = clientToUseForMessage; } const whitelist = perMessageClientOptionsWhitelist[clientToUseForMessage]; if (!whitelist) { // No whitelist, return all options return inputOptions; } const outputOptions = { clientToUse: clientToUseForMessage, }; for (const property of Object.keys(inputOptions)) { const allowed = whitelist.includes(property); if (!allowed && typeof inputOptions[property] === 'object') { // Check for nested properties for (const nestedProp of Object.keys(inputOptions[property])) { const nestedAllowed = whitelist.includes(`${property}.${nestedProp}`); if (nestedAllowed) { outputOptions[property] = outputOptions[property] || {}; outputOptions[property][nestedProp] = inputOptions[property][nestedProp]; } } continue; } // Copy allowed properties to outputOptions if (allowed) { outputOptions[property] = inputOptions[property]; } } return outputOptions; }