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