chatgpt-optimized-official
Version:
ChatGPT Client using official OpenAI API
429 lines • 18.7 kB
JavaScript
import axios from "axios";
import { randomUUID } from "crypto";
import { encode } from "gpt-3-encoder";
import MessageType from "../enums/message-type.js";
import AppDbContext from "./app-dbcontext.js";
class ChatGPT {
options;
db;
onUsage;
constructor(key, options) {
this.db = new AppDbContext();
this.db.WaitForLoad().then(() => {
if (typeof key === "string") {
if (this.db.keys.Any((x) => x.key === key))
return;
this.db.keys.Add({
key: key,
queries: 0,
balance: 0,
tokens: 0,
});
}
else if (Array.isArray(key)) {
key.forEach((k) => {
if (this.db.keys.Any((x) => x.key === k))
return;
this.db.keys.Add({
key: k,
queries: 0,
balance: 0,
tokens: 0,
});
});
}
});
this.options = {
model: options?.model || "gpt-3.5-turbo",
temperature: options?.temperature || 0.7,
max_tokens: options?.max_tokens || 100,
top_p: options?.top_p || 0.9,
frequency_penalty: options?.frequency_penalty || 0,
presence_penalty: options?.presence_penalty || 0,
instructions: options?.instructions || `You are ChatGPT, a language model developed by OpenAI. You are designed to respond to user input in a conversational manner, Answer as concisely as possible. Your training data comes from a diverse range of internet text and You have been trained to generate human-like responses to various questions and prompts. You can provide information on a wide range of topics, but your knowledge is limited to what was present in your training data, which has a cutoff date of 2021. You strive to provide accurate and helpful information to the best of your ability.\nKnowledge cutoff: 2021-09`,
price: options?.price || 0.002,
max_conversation_tokens: options?.max_conversation_tokens || 4097,
endpoint: options?.endpoint || "https://api.openai.com/v1/chat/completions",
moderation: options?.moderation || false,
functions: options?.functions || null,
function_call: options?.function_call || null,
tools: options?.tools || null,
tool_choice: options?.tool_choice || 'auto',
parallel_tool_calls: options?.parallel_tool_calls || false,
response_format: options?.response_format || null,
};
}
getOpenAIKey() {
let key = this.db.keys.OrderBy((x) => x.balance).FirstOrDefault();
if (key == null) {
key = this.db.keys.FirstOrDefault();
}
if (key == null) {
throw new Error("No keys available.");
}
return key;
}
async *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) {
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 *linesToMessages(linesAsync) {
for await (const line of linesAsync) {
const message = line.substring("data :".length);
yield message;
}
}
async *streamCompletion(data) {
yield* this.linesToMessages(this.chunksToLines(data));
}
getInstructions(username) {
return `${this.options.instructions}
Current date: ${this.getToday()}
Current time: ${this.getTime()}${username !== "User" ? `\nName of the user talking to: ${username}` : ""}`;
}
addConversation(conversationId, userName = "User") {
let conversation = {
id: conversationId,
userName: userName,
messages: [],
};
this.db.conversations.Add(conversation);
return conversation;
}
getConversation(conversationId, userName = "User") {
let conversation = this.db.conversations.Where((conversation) => conversation.id === conversationId).FirstOrDefault();
if (!conversation) {
conversation = this.addConversation(conversationId, userName);
}
else {
conversation.lastActive = Date.now();
}
conversation.userName = userName;
return conversation;
}
resetConversation(conversationId) {
let conversation = this.db.conversations.Where((conversation) => conversation.id == conversationId).FirstOrDefault();
if (conversation) {
conversation.messages = [];
conversation.lastActive = Date.now();
}
return conversation;
}
async ask(prompt, conversationId = "default", userName = "User") {
return await this.askStream((data) => { }, (data) => { }, prompt, conversationId, userName);
}
async askStream(data, usage, prompt, conversationId = "default", userName = "User") {
let oAIKey = this.getOpenAIKey();
let conversation = this.getConversation(conversationId, userName);
if (this.options.moderation) {
let flagged = await this.moderate(prompt, oAIKey.key);
if (flagged) {
for (let chunk in "Your message was flagged as inappropriate and was not sent.".split("")) {
data(chunk);
await this.wait(100);
}
return "Your message was flagged as inappropriate and was not sent.";
}
}
let promptStr = this.generatePrompt(conversation, prompt);
let prompt_tokens = this.countTokens(promptStr);
try {
const response = await axios.post(this.options.endpoint, {
model: this.options.model,
messages: promptStr,
temperature: this.options.temperature,
max_tokens: this.options.max_tokens,
top_p: this.options.top_p,
frequency_penalty: this.options.frequency_penalty,
presence_penalty: this.options.presence_penalty,
stream: true
}, {
responseType: "stream",
headers: {
Accept: "text/event-stream",
"Content-Type": "application/json",
Authorization: `Bearer ${oAIKey.key}`,
},
});
let responseStr = "";
let responseArg = "";
let responseNameFunction = "";
for await (const message of this.streamCompletion(response.data)) {
try {
const parsed = JSON.parse(message);
const { delta, finish_reason } = parsed.choices[0];
const { content, function_call } = delta;
if (function_call) {
responseNameFunction += function_call.name;
responseArg += function_call.arguments;
}
if (finish_reason === "function_call") {
responseStr = JSON.stringify({ "name": responseNameFunction, "arguments": responseArg });
data(responseStr);
}
else if (content) {
responseStr += content;
data(content);
}
}
catch (error) {
console.error("Could not JSON parse stream message", message, error);
}
}
let completion_tokens = encode(responseStr).length;
let usageData = {
key: oAIKey.key,
prompt_tokens: prompt_tokens,
completion_tokens: completion_tokens,
total_tokens: prompt_tokens + completion_tokens,
};
usage(usageData);
if (this.onUsage)
this.onUsage(usageData);
oAIKey.tokens += usageData.total_tokens;
oAIKey.balance = (oAIKey.tokens / 1000) * this.options.price;
oAIKey.queries++;
conversation.messages.push({
id: randomUUID(),
content: responseStr,
type: MessageType.Assistant,
date: Date.now(),
});
return responseStr;
}
catch (error) {
if (error.response && error.response.data && error.response.headers["content-type"] === "application/json") {
let errorResponseStr = "";
for await (const message of error.response.data) {
errorResponseStr += message;
}
const errorResponseJson = JSON.parse(errorResponseStr);
throw new Error(errorResponseJson.error.message);
}
else {
throw new Error(error.message);
}
}
}
async askV1(prompt, conversationId = "default", type = 1, function_name, tool_call_id, userName = "User", response_format) {
return await this.askPost((data) => { }, (data) => { }, prompt, conversationId, function_name, userName, type, tool_call_id, response_format);
}
async askPost(data, usage, prompt, conversationId = "default", function_name, userName = "User", type = MessageType.User, tool_call_id, response_format) {
let oAIKey = this.getOpenAIKey();
let conversation = this.getConversation(conversationId, userName);
let promptStr = this.generatePrompt(conversation, prompt, type, function_name, tool_call_id);
let prompt_tokens = this.countTokens(promptStr);
try {
let auxOptions = {
model: this.options.model,
messages: promptStr,
temperature: this.options.temperature,
max_tokens: this.options.max_tokens,
top_p: this.options.top_p,
frequency_penalty: this.options.frequency_penalty,
presence_penalty: this.options.presence_penalty,
stream: false,
tools: this.options.tools,
tool_choice: this.options.tool_choice,
parallel_tool_calls: this.options.parallel_tool_calls,
response_format: response_format,
};
if (this.options.functions) {
auxOptions["functions"] = this.options.functions;
auxOptions["function_call"] = this.options.function_call ? this.options.function_call : "auto";
}
const response = await axios.post(this.options.endpoint, auxOptions, {
responseType: "json",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${oAIKey.key}`,
},
});
let completion_tokens = response.data.usage['completion_tokens'];
let usageData = {
key: oAIKey.key,
prompt_tokens: prompt_tokens,
completion_tokens: completion_tokens,
total_tokens: prompt_tokens + completion_tokens,
};
if (this.onUsage)
this.onUsage(usageData);
oAIKey.tokens += usageData.total_tokens;
oAIKey.balance = (oAIKey.tokens / 1000) * this.options.price;
oAIKey.queries++;
if (response.data.choices[0].finish_reason == "tool_calls") {
conversation.messages.push({
id: randomUUID(),
content: response.data.choices[0]['message']['content'],
type: MessageType.Assistant,
date: Date.now(),
name: function_name,
tool_calls: response.data.choices[0]['message']['tool_calls'],
});
}
else if (response.data.choices[0]['message']['content']) {
conversation.messages.push({
id: randomUUID(),
content: response.data.choices[0]['message']['content'] ? response.data.choices[0]['message']['content'] : "",
type: MessageType.Assistant,
date: Date.now(),
});
}
data(JSON.stringify(response.data.choices[0]));
return response.data.choices[0];
}
catch (error) {
if (error.response && error.response.data && error.response.headers["content-type"] === "application/json") {
throw new Error(error.response.data.error.message);
}
else {
throw new Error(error.message);
}
}
}
async moderate(prompt, key) {
return false;
}
generatePrompt(conversation, prompt, type = MessageType.User, function_name, tool_call_id) {
if (Array.isArray(prompt) && prompt.length > 0 && 'tool_call_id' in prompt[0]) {
const toolOutputs = prompt;
for (const toolOutput of toolOutputs) {
conversation.messages.push({
id: randomUUID(),
content: toolOutput.content,
type: MessageType.Function,
date: Date.now(),
name: toolOutput.name,
tool_call_id: toolOutput.tool_call_id,
});
}
}
else {
let message = {
id: randomUUID(),
content: prompt,
type: type,
date: Date.now(),
};
if (type === MessageType.Function && function_name) {
message["name"] = function_name;
message["tool_call_id"] = tool_call_id;
}
conversation.messages.push(message);
}
let messages = this.generateMessages(conversation);
let promptEncodedLength = this.countTokens(messages);
let totalLength = promptEncodedLength + this.options.max_tokens;
while (totalLength > this.options.max_conversation_tokens) {
conversation.messages.shift();
messages = this.generateMessages(conversation);
promptEncodedLength = this.countTokens(messages);
totalLength = promptEncodedLength + this.options.max_tokens;
}
conversation.lastActive = Date.now();
return messages;
}
generateMessages(conversation) {
let messages = [];
messages.push({
role: "system",
content: this.getInstructions(conversation.userName),
});
for (let message of conversation.messages) {
if (message.type === MessageType.Function) {
messages.push({
tool_call_id: message.tool_call_id,
role: "tool",
name: message.name || "unknownFunction",
content: message.content,
});
}
else if (message.type === MessageType.User) {
messages.push({
role: "user",
content: message.content,
});
}
else {
if (message.tool_calls) {
messages.push({
role: "assistant",
content: message.content,
tool_calls: message.tool_calls,
});
}
else {
messages.push({
role: "assistant",
content: message.content,
});
}
}
}
return messages;
}
countTokens(messages) {
let tokens = 0;
for (let message of messages) {
if (message.content) {
if (typeof message.content === "string") {
tokens += encode(message.content).length;
}
else if (Array.isArray(message.content)) {
for (let contentBlock of message.content) {
if (contentBlock.type === "text") {
tokens += encode(contentBlock.text).length;
}
else if (contentBlock.type === "image_url") {
if (contentBlock.image_url.detail === "low" || !contentBlock.image_url.detail) {
tokens += 85;
}
else if (contentBlock.image_url.detail === "high") {
tokens += 765;
}
else {
tokens += 85;
}
}
}
}
}
}
return tokens;
}
getToday() {
let today = new Date();
let dd = String(today.getDate()).padStart(2, "0");
let mm = String(today.getMonth() + 1).padStart(2, "0");
let yyyy = today.getFullYear();
return `${yyyy}-${mm}-${dd}`;
}
getTime() {
let today = new Date();
let hours = today.getHours();
let minutes = today.getMinutes();
let ampm = hours >= 12 ? "PM" : "AM";
hours = hours % 12;
hours = hours ? hours : 12;
minutes = minutes < 10 ? `0${minutes}` : minutes;
return `${hours}:${minutes} ${ampm}`;
}
wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
export default ChatGPT;
//# sourceMappingURL=chatgpt.js.map