From 85bf57fcac2ad41921ea485da2b1a035818c89f9 Mon Sep 17 00:00:00 2001 From: Pawan Osman Date: Fri, 24 Mar 2023 07:19:18 +0300 Subject: [PATCH] update --- LICENSE | 2 +- config.js | 23 +++++ functions.js | 45 +++++++++ index.js | 33 ++++++ middlewares.js | 46 +++++++++ package.json | 20 ++++ routes.js | 270 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 config.js create mode 100644 functions.js create mode 100644 index.js create mode 100644 middlewares.js create mode 100644 package.json create mode 100644 routes.js diff --git a/LICENSE b/LICENSE index 06720d7..41a6f75 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Pawan Osman +Copyright (c) 2023 Pawan Osman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/config.js b/config.js new file mode 100644 index 0000000..afc7b48 --- /dev/null +++ b/config.js @@ -0,0 +1,23 @@ +// Server configuration +export const SERVER_PORT = 3000; // Server port +export const DEBUG = false; // Debug mode + +// Prompt Moderation before sending to OpenAI +export const MODERATION = true; // Moderation mode + +// Rate limit +export const PRIOD = 15 * 1000; // 15 seconds +export const RATE_LIMIT = 50; // 50 requests per 15 seconds + +// Whitelisted IPs +export const WHITELISTED_IPS = [ + // "127.0.0.1" +]; + +// OpenAI API Keys +export let OPENAI_KEYS = [ + "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", +]; \ No newline at end of file diff --git a/functions.js b/functions.js new file mode 100644 index 0000000..4efffae --- /dev/null +++ b/functions.js @@ -0,0 +1,45 @@ +import { OPENAI_KEYS } from "./config.js"; + +async function* chunksToLines(chunksAsync) { + let previous = ""; + for await (const chunk of chunksAsync) { + const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + previous += bufferChunk; + let eolIndex; + while ((eolIndex = previous.indexOf("\n")) >= 0) { + // line includes the EOL + const line = previous.slice(0, eolIndex + 1).trimEnd(); + if (line === "data: [DONE]") break; + if (line.startsWith("data: ")) yield line; + previous = previous.slice(eolIndex + 1); + } + } +} + +async function* linesToMessages(linesAsync) { + for await (const line of linesAsync) { + const message = line.substring("data :".length); + + yield message; + } +} + +async function* streamCompletion(data) { + yield* linesToMessages(chunksToLines(data)); +} + +function generateId() { + const chars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let id = "org-"; + for (let i = 0; i < 24; i++) { + id += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return id; +} + +function getOpenAIKey() { + return OPENAI_KEYS[Math.floor(Math.random() * OPENAI_KEYS.length)]; +} + +export { generateId, getOpenAIKey, streamCompletion } \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..a726a80 --- /dev/null +++ b/index.js @@ -0,0 +1,33 @@ +import express, { json, urlencoded } from 'express'; +import { completions, chatCompletions } from './routes.js'; +import { corsMiddleware, rateLimitMiddleware } from './middlewares.js'; +import { SERVER_PORT } from './config.js'; + +let app = express(); + +process.on("uncaughtException", function (err) { + if (DEBUG) console.error(`Caught exception: ${err}`); +}); + +// Middlewares +app.use(corsMiddleware); +app.use(rateLimitMiddleware); +app.use(json()); +app.use(urlencoded({ extended: true })); + +// Register routes +app.all("/", async function (req, res) { + res.set("Content-Type", "application/json"); + return res.status(200).send({ + status: true, + github: "https://github.com/PawanOsman/ChatGPT", + discord: "https://discord.pawan.krd" + }); +}); +app.post("/api/completions", completions); +app.post("/api/chat/completions", chatCompletions); + +// Start server +app.listen(SERVER_PORT, () => { + console.log(`Listening on ${SERVER_PORT} ...`); +}); \ No newline at end of file diff --git a/middlewares.js b/middlewares.js new file mode 100644 index 0000000..2fbf207 --- /dev/null +++ b/middlewares.js @@ -0,0 +1,46 @@ +import { RATE_LIMIT, PRIOD, WHITELISTED_IPS } from "./config.js"; + +const rateLimit = new Map(); + +function corsMiddleware(req, res, next) { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Headers", "*"); + res.header("Access-Control-Allow-Methods", "*"); + next(); +}; + +async function rateLimitMiddleware(req, res, next) { + let ip = req.headers["CF-Connecting-IP"] ?? req.headers["cf-connecting-ip"] ?? req.headers["X-Forwarded-For"] ?? req.headers["x-forwarded-for"] ?? req.src.ip; + if (WHITELISTED_IPS.includes(ip)) return next(); + if (!rateLimit.has(ip)) { + rateLimit.set(ip, { + requests: 1, + lastRequestTime: Date.now() + }); + } else { + const currentTime = Date.now(); + const timeSinceLastRequest = currentTime - rateLimit.get(ip).lastRequestTime; + if (timeSinceLastRequest > PRIOD) { + rateLimit.set(ip, { + requests: 1, + lastRequestTime: currentTime + }); + } else { + let updatedCount = rateLimit.get(ip).requests + 1; + if (updatedCount > RATE_LIMIT) { + return res.status(429).send({ + status: false, + error: "Too many requests, please try again later" + }); + } + rateLimit.set(ip, { + requests: updatedCount, + lastRequestTime: rateLimit.get(ip).lastRequestTime + }); + } + } + + next(); +}; + +export { corsMiddleware, rateLimitMiddleware } \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..acb7580 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "chatgpt", + "version": "1.0.0", + "description": "## If you have any questions or need assistance, please join [[Discord](https://discord.pawan.krd)]", + "type": "module", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/PawanOsman/ChatGPT.git" + }, + "author": "PawanOsman (https://pawan.krd/)", + "license": "MIT", + "bugs": { + "url": "https://github.com/PawanOsman/ChatGPT/issues" + }, + "homepage": "https://github.com/PawanOsman/ChatGPT#readme" +} diff --git a/routes.js b/routes.js new file mode 100644 index 0000000..15d93c9 --- /dev/null +++ b/routes.js @@ -0,0 +1,270 @@ +import axios from "axios"; +import { Configuration, OpenAIApi } from "openai"; +import { streamCompletion, generateId, getOpenAIKey } from "./functions.js" +import { DEBUG, MODERATION } from "./config.js"; + +async function completions(req, res) { + let orgId = generateId(); + let key = getOpenAIKey(); + + if (!req.body.prompt) { + res.set("Content-Type", "application/json"); + return res.status(400).send({ + status: false, + error: "No prompt provided" + }); + } + + if (DEBUG) console.log(`[Text] [${req.user.data.id}] [${req.user.data.name}] [MAX-TOKENS:${req.body.max_tokens ?? "unset"}] ${req.body.prompt}`); + + if (MODERATION) { + try { + let openAi = new OpenAIApi(new Configuration({ apiKey: key.apikey })); + let response = await openAi.createModeration({ + input: req.body.prompt, + }); + + if (response.data.results[0].flagged) { + res.set("Content-Type", "application/json"); + return res.status(400).send({ + status: false, + error: "Your prompt contains content that is not allowed", + reason: response.data.results[0].reason, + contact: "https://discord.pawan.krd" + }); + } + } + catch (e) { + + } + } + + if (req.body.stream) { + try { + const response = await axios.post( + `https://api.openai.com/v1/completions`, req.body, + { + responseType: "stream", + headers: { + Accept: "text/event-stream", + "Content-Type": "application/json", + Authorization: `Bearer ${key.apikey}`, + }, + }, + ); + + res.setHeader("content-type", "text/event-stream"); + + for await (const message of streamCompletion(response.data)) { + try { + const parsed = JSON.parse(message); + delete parsed.id; + delete parsed.created; + res.write(`data: ${JSON.stringify(parsed)}\n\n`); + } catch (error) { + if (DEBUG) console.error("Could not JSON parse stream message", message, error); + } + } + + res.write(`data: [DONE]`); + res.end(); + } catch (error) { + try { + if (error.response && error.response.data) { + let errorResponseStr = ""; + + for await (const message of error.response.data) { + errorResponseStr += message; + } + + errorResponseStr = errorResponseStr.replace(/org-[a-zA-Z0-9]+/, orgId); + + const errorResponseJson = JSON.parse(errorResponseStr); + return res.status(error.response.status).send(errorResponseJson); + } else { + if (DEBUG) console.error("Could not JSON parse stream message", error); + return res.status(500).send({ + status: false, + error: "something went wrong!" + }); + } + } + catch (e) { + console.log(e); + return res.status(500).send({ + status: false, + error: "something went wrong!" + }); + } + } + } + else { + try { + const response = await axios.post( + `https://api.openai.com/v1/completions`, req.body, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${key.apikey}`, + }, + }, + ); + + delete response.data.id; + delete response.data.created; + + return res.status(200).send(response.data); + } catch (error) { + try { + error.response.data.error.message = error.response.data.error.message.replace(/org-[a-zA-Z0-9]+/, orgId); + return res.status(error.response.status).send(error.response.data); + } + catch (e) { + if (DEBUG) console.log(e); + return res.status(500).send({ + status: false, + error: "something went wrong!" + }); + } + } + } +} + +async function chatCompletions(req, res) { + let orgId = generateId(); + let key = getOpenAIKey(true); + + if (DEBUG) console.log(`[CHAT] [${req.user.data.id}] [${req.user.data.name}] [MAX-TOKENS:${req.body.max_tokens ?? "unset"}] ${prompt}`); + + if (MODERATION) { + try { + let prompt = ""; + try { + req.body.messages.forEach(element => { + prompt += element.content; + }); + } + catch (e) { + return res.status(400).send({ + status: false, + error: "messages is required! and must be an array of objects with content and author properties" + }); + } + + let openAi = new OpenAIApi(new Configuration({ apiKey: key.apikey })); + let response = await openAi.createModeration({ + input: prompt, + }); + + if (response.data.results[0].flagged) { + res.set("Content-Type", "application/json"); + return res.status(400).send({ + status: false, + error: "Your prompt contains content that is not allowed", + reason: response.data.results[0].reason, + support: "https://discord.pawan.krd" + }); + } + } + catch (e) { + + } + } + + if (req.body.stream) { + try { + const response = await axios.post( + `https://api.openai.com/v1/chat/completions`, req.body, + { + responseType: "stream", + headers: { + Accept: "text/event-stream", + "Content-Type": "application/json", + Authorization: `Bearer ${key.apikey}`, + }, + }, + ); + + res.setHeader("content-type", "text/event-stream"); + + for await (const message of streamCompletion(response.data)) { + try { + const parsed = JSON.parse(message); + delete parsed.id; + delete parsed.created; + const { content } = parsed.choices[0].delta; + if (content) { + res.write(`data: ${JSON.stringify(parsed)}\n\n`); + } + } catch (error) { + if (DEBUG) console.error("Could not JSON parse stream message", message, error); + } + } + + res.write(`data: [DONE]`); + res.end(); + } catch (error) { + try { + if (error.response && error.response.data) { + let errorResponseStr = ""; + + for await (const message of error.response.data) { + errorResponseStr += message; + } + + errorResponseStr = errorResponseStr.replace(/org-[a-zA-Z0-9]+/, orgId); + + const errorResponseJson = JSON.parse(errorResponseStr); + return res.status(error.response.status).send(errorResponseJson); + } else { + if (DEBUG) console.error("Could not JSON parse stream message", error); + return res.status(500).send({ + status: false, + error: "something went wrong!" + }); + } + } + catch (e) { + if (DEBUG) console.log(e); + return res.status(500).send({ + status: false, + error: "something went wrong!" + }); + } + } + } + else { + try { + const response = await axios.post( + `https://api.openai.com/v1/chat/completions`, req.body, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${key.apikey}`, + }, + }, + ); + + delete response.data.id; + delete response.data.created; + + return res.status(200).send(response.data); + } catch (error) { + try { + error.response.data.error.message = error.response.data.error.message.replace(/org-[a-zA-Z0-9]+/, orgId); + return res.status(error.response.status).send(error.response.data); + } + catch (e) { + if (DEBUG) console.log(e); + return res.status(500).send({ + status: false, + error: "something went wrong!" + }); + } + } + } +} + +export { completions, chatCompletions }; \ No newline at end of file