oai-proxy / src /keys.ts
Xeeph's picture
Duplicate from idosal/oai-proxy
5bddbd8
raw
history blame contribute delete
No virus
4.1 kB
/* Manages OpenAI API keys. Tracks usage, disables expired keys, and provides
round-robin access to keys. Keys are stored in the OPENAI_KEY environment
variable, either as a single key, or a base64-encoded JSON array of keys.*/
import { logger } from "./logger";
import crypto from "crypto";
/** Represents a key stored in the OPENAI_KEY environment variable. */
type KeySchema = {
/** The OpenAI API key itself. */
key: string;
/** Whether this is a free trial key. These are prioritized over paid keys if they can fulfill the request. */
isTrial?: boolean;
/** Whether this key has been provisioned for GPT-4. */
isGpt4?: boolean;
};
/** Runtime information about a key. */
export type Key = KeySchema & {
/** Whether this key is currently disabled. We set this if we get a 429 or 401 response from OpenAI. */
isDisabled?: boolean;
/** Threshold at which a warning email will be sent by OpenAI. */
softLimit?: number;
/** Threshold at which the key will be disabled because it has reached the user-defined limit. */
hardLimit?: number;
/** The maximum quota allocated to this key by OpenAI. */
systemHardLimit?: number;
/** The current usage of this key. */
usage?: number;
/** The number of prompts that have been sent with this key. */
promptCount: number;
/** The time at which this key was last used. */
lastUsed: number;
/** Key hash for displaying usage in the dashboard. */
hash: string;
};
const keyPool: Key[] = [];
function init() {
const keyString = process.env.OPENAI_KEY;
if (!keyString?.trim()) {
throw new Error("OPENAI_KEY environment variable is not set");
}
let keyList: KeySchema[];
try {
const decoded = Buffer.from(keyString, "base64").toString();
keyList = JSON.parse(decoded) as KeySchema[];
} catch (err) {
logger.info("OPENAI_KEY is not base64-encoded JSON, assuming bare key");
// We don't actually know if bare keys are paid/GPT-4 so we assume they are
keyList = [{ key: keyString, isTrial: false, isGpt4: true }];
}
for (const key of keyList) {
const newKey = {
...key,
isDisabled: false,
softLimit: 0,
hardLimit: 0,
systemHardLimit: 0,
usage: 0,
lastUsed: 0,
promptCount: 0,
hash: crypto
.createHash("sha256")
.update(key.key)
.digest("hex")
.slice(0, 6),
};
keyPool.push(newKey);
logger.info({ key: newKey.hash }, "Key added");
}
// TODO: check each key's usage upon startup.
}
function list() {
return keyPool.map((key) => ({
...key,
key: undefined,
}));
}
function disable(key: Key) {
const keyFromPool = keyPool.find((k) => k.key === key.key)!;
if (keyFromPool.isDisabled) return;
keyFromPool.isDisabled = true;
logger.warn({ key: key.hash }, "Key disabled");
}
function anyAvailable() {
return keyPool.some((key) => !key.isDisabled);
}
function get(model: string) {
const needsGpt4Key = model.startsWith("gpt-4");
const availableKeys = keyPool.filter(
(key) => !key.isDisabled && (!needsGpt4Key || key.isGpt4)
);
if (availableKeys.length === 0) {
let message = "No keys available. Please add more keys.";
if (needsGpt4Key) {
message =
"No GPT-4 keys available. Please add more keys or use a non-GPT-4 model.";
}
logger.error(message);
throw new Error(message);
}
// Prioritize trial keys
const trialKeys = availableKeys.filter((key) => key.isTrial);
if (trialKeys.length > 0) {
logger.info({ key: trialKeys[0].hash }, "Using trial key");
trialKeys[0].lastUsed = Date.now();
return trialKeys[0];
}
// Otherwise, return the oldest key
const oldestKey = availableKeys.sort((a, b) => a.lastUsed - b.lastUsed)[0];
logger.info({ key: oldestKey.hash }, "Assigning key to request.");
oldestKey.lastUsed = Date.now();
return { ...oldestKey };
}
function incrementPrompt(keyHash?: string) {
if (!keyHash) return;
const key = keyPool.find((k) => k.hash === keyHash)!;
key.promptCount++;
}
export const keys = { init, list, get, anyAvailable, disable, incrementPrompt };