/* 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 };