V-1488abed / src /proxies.ts
krazyxki's picture
Duplicate from neurokun/V-1488
29ce54a
import crypto from "crypto";
import axios from "axios";
import { minutesToMilliseconds } from 'date-fns';
import { URL } from 'url';
import { config } from "./config";
import { logger } from "./logger";
const CHECK_INTERVAL = minutesToMilliseconds(3);
/** Runtime information about a proxy. */
export type Proxy = {
url: string;
isGpt4: boolean;
/** Whether this proxy is currently disabled. We set this if we get a 429 or 401 response from OpenAI. */
isDisabled?: boolean;
/** The number of prompts that have been sent with this proxy. */
promptCount: number;
/** The time at which this proxy was last used. */
lastUsed: number;
/** Proxy hash for displaying usage in the dashboard. */
hash: string;
remaining: number;
isLogging?: boolean;
};
const proxyPool: Proxy[] = [];
async function init() {
setTimeout(checker, CHECK_INTERVAL);
const proxiestring = config.openaiProxy;
const proxyList = parse(proxiestring);
for (const proxy of proxyList) {
await add(proxy);
}
}
async function checker() {
for (const proxy of proxyPool) {
await check(proxy, true);
}
setTimeout(checker, CHECK_INTERVAL);
}
function list() {
return proxyPool.map((proxy) => ({
...proxy,
url: undefined,
}));
}
function parse(proxiestring: string | undefined): string[] {
if (!proxiestring) {
return [];
}
try {
const decoded = Buffer.from(proxiestring, "base64").toString();
return JSON.parse(decoded);
} catch (err) {
logger.info("Proxy is not base64-encoded JSON, assuming bare url");
return [proxiestring];
}
}
async function check(proxy: Proxy, silent = false) {
let isDisabled = false;
let isGpt4 = false;
let rateLimit = 0;
let isLogging = false;
try {
const href = new URL(proxy.url);
href.pathname = '';
href.search = '';
const { data } = await axios.get(href.toString());
const startIdx = data.indexOf('<pre>{');
const endIdx = data.indexOf('}</pre>');
if (startIdx >= 0 && endIdx >= 0) {
const slice = data.slice(startIdx + 5, endIdx + 1);
try {
const config = JSON.parse(slice);
if (config['gpt-4']?.active > 0 && config['gpt-4']?.remaining !== '0%') {
isGpt4 = true;
}
if (config.keys?.gpt4 > 0 && config.keys?.quotaLeft !== '0%') {
isGpt4 = true;
}
if (config.keyInfo?.gpt4 > 0 && config.keyInfo?.quotaLeft?.gpt4 !== '0%') {
isGpt4 = true;
}
if (config.keys?.active === 0) {
isDisabled = true;
}
if (config.keyInfo?.active === 0) {
isDisabled = true;
}
if (config.config?.proxyKey) {
isDisabled = true;
}
if (config.config?.promptLogging === 'true') {
isLogging = true;
}
const quotaLeft = isGpt4
? parseInt(config.keys?.quotaLeft.gpt4 || config.keyInfo?.quotaLeft?.gpt4 || config['gpt-4']?.remaining)
: parseInt(config.keys?.quotaLeft.all || config.keyInfo?.quotaLeft?.all || config['gpt-3.5-turbo']?.remaining);
if (!isNaN(quotaLeft)) {
proxy.remaining = quotaLeft;
}
rateLimit = +config.config?.modelRateLimit || 0;
} catch (e) { }
}
if (!isDisabled && rateLimit > 2 && !isLogging) {
try {
await axios.post(
`${proxy.url}/v1/chat/completions`,
{
model: "gpt-3.5-turbo", max_tokens: 1,
messages: [{ role: "user", content: "" }],
},
{ headers: { "Content-Type": "application/json" } },
);
} catch (e) {
if (!silent) logger.error(e, `Proxy is not active ${proxy.url}`);
isDisabled = true;
}
}
if (!isDisabled && isGpt4 && rateLimit > 2 && !isLogging) {
try {
await axios.post(
`${proxy.url}/v1/chat/completions`,
{
model: "gpt-4", max_tokens: 1,
messages: [{ role: "user", content: "" }],
},
{ headers: { "Content-Type": "application/json" } },
);
} catch (e) {
if (!silent) logger.error(e, `Proxy is not GPT-4 ${proxy.url}`);
isGpt4 = false;
}
}
} catch (e) {
isDisabled = true;
if (!silent) logger.error(e, `Error checking proxy ${proxy.url}`);
}
proxy.isDisabled = isDisabled;
proxy.isGpt4 = isGpt4;
proxy.isLogging = isLogging;
}
async function add(url: string) {
if (!url) {
return;
}
if (proxyPool.some(k => k.url === url)) {
logger.warn('Proxy already exists');
return;
}
const newProxy = {
url,
isGpt4: false,
isDisabled: false,
lastUsed: 0,
promptCount: 0,
remaining: 100,
hash: crypto
.createHash("sha256")
.update(url)
.digest("hex")
.slice(0, 6),
};
await check(newProxy);
proxyPool.push(newProxy);
logger.info({ proxy: newProxy.hash }, "Proxy added");
return newProxy;
}
function disable(proxy: Proxy) {
const proxyFromPool = proxyPool.find((k) => k.url === proxy.url)!;
if (proxyFromPool.isDisabled) return;
proxyFromPool.isDisabled = true;
logger.warn({ proxy: proxy.hash, url: proxy.url }, "Proxy disabled");
}
function anyAvailable() {
return proxyPool.some((proxy) => !proxy.isDisabled);
}
function get(model: string) {
const needsGpt4Proxy = model.startsWith("gpt-4");
const enabledProxies = proxyPool.filter(
(proxy) => !proxy.isDisabled
);
let availableProxies = enabledProxies.filter(k => k.isGpt4 === needsGpt4Proxy);
if (availableProxies.length === 0 && !needsGpt4Proxy) {
availableProxies = enabledProxies;
}
if (availableProxies.length === 0) {
let message = "No proxies available. Please add more proxies.";
if (needsGpt4Proxy) {
message =
"No GPT-4 proxies available. Please add more proxies or use a non-GPT-4 model.";
}
logger.error(message);
return;
}
const notLoggingProxies = availableProxies.filter((proxy) => !proxy.isLogging);
const selectionProxies = notLoggingProxies.length > 0 ? notLoggingProxies : availableProxies;
const msg = notLoggingProxies.length > 0 ? "Assigning proxy to request." : "Using logging proxy";
// Return the oldest proxy
const oldestProxy = selectionProxies.sort((a, b) => a.lastUsed - b.lastUsed)[0];
logger.info({ proxy: oldestProxy.hash }, msg);
oldestProxy.lastUsed = Date.now();
return { ...oldestProxy };
}
function incrementPrompt(proxyHash?: string) {
if (!proxyHash) return;
const proxy = proxyPool.find((k) => k.hash === proxyHash)!;
proxy.promptCount++;
}
function downgradeProxy(proxyHash?: string) {
if (!proxyHash) return;
logger.warn({ proxy: proxyHash }, "Downgrading proxy to GPT-3.5.");
const proxy = proxyPool.find((k) => k.hash === proxyHash)!;
proxy.isGpt4 = false;
}
function getRemaining(onlyActive = false) {
let remaining = 0;
let remainingCount = 0;
for (const proxy of proxyPool) {
if (onlyActive && proxy.isDisabled) continue;
remainingCount++;
remaining += proxy.remaining;
}
if (remainingCount === 0) return '0%';
return `${Math.round(remaining / remainingCount)}%`;
}
export const proxies = {
init,
list,
get,
anyAvailable,
parse,
add,
check,
getRemaining,
disable,
incrementPrompt,
downgradeProxy,
};