File size: 4,104 Bytes
4e08f6a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
/* 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 };
|