nodejs-gpt
Version:
A ChatGPT implementation using the official ChatGPT model via OpenAI's API.
249 lines (219 loc) • 8.29 kB
JavaScript
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;
}