2023-04-08 17:32:49 -06:00
|
|
|
import dotenv from "dotenv";
|
2023-05-13 22:26:08 -06:00
|
|
|
import type firebase from "firebase-admin";
|
2023-04-08 17:32:49 -06:00
|
|
|
dotenv.config();
|
|
|
|
|
2023-04-10 04:39:13 -06:00
|
|
|
const isDev = process.env.NODE_ENV !== "production";
|
|
|
|
|
2023-05-09 17:11:57 -06:00
|
|
|
type PromptLoggingBackend = "google_sheets";
|
|
|
|
export type DequeueMode = "fair" | "random" | "none";
|
2023-04-14 19:21:04 -06:00
|
|
|
|
2023-04-08 17:32:49 -06:00
|
|
|
type Config = {
|
|
|
|
/** The port the proxy server will listen on. */
|
|
|
|
port: number;
|
2023-04-14 19:21:04 -06:00
|
|
|
/** OpenAI API key, either a single key or a comma-delimeted list of keys. */
|
2023-04-08 17:32:49 -06:00
|
|
|
openaiKey?: string;
|
2023-05-12 18:58:15 -06:00
|
|
|
/**
|
|
|
|
* The proxy key to require for requests. Only applicable if the user
|
|
|
|
* management mode is set to 'proxy_key', and required if so.
|
|
|
|
**/
|
2023-04-08 17:32:49 -06:00
|
|
|
proxyKey?: string;
|
2023-05-12 18:58:15 -06:00
|
|
|
/**
|
2023-05-13 22:26:08 -06:00
|
|
|
* The admin key used to access the /admin API. Required if the user
|
2023-05-12 18:58:15 -06:00
|
|
|
* management mode is set to 'user_token'.
|
|
|
|
**/
|
|
|
|
adminKey?: string;
|
|
|
|
/**
|
|
|
|
* Which user management mode to use.
|
|
|
|
*
|
|
|
|
* `none`: No user management. Proxy is open to all requests with basic
|
|
|
|
* abuse protection.
|
|
|
|
*
|
|
|
|
* `proxy_key`: A specific proxy key must be provided in the Authorization
|
|
|
|
* header to use the proxy.
|
|
|
|
*
|
|
|
|
* `user_token`: Users must be created via the /admin REST API and provide
|
|
|
|
* their personal access token in the Authorization header to use the proxy.
|
|
|
|
* Configure this function and add users via the /admin API.
|
|
|
|
*/
|
|
|
|
gatekeeper: "none" | "proxy_key" | "user_token";
|
2023-05-13 22:26:08 -06:00
|
|
|
/**
|
|
|
|
* Persistence layer to use for user management.
|
|
|
|
*
|
|
|
|
* `memory`: Users are stored in memory and are lost on restart (default)
|
|
|
|
*
|
|
|
|
* `firebase_rtdb`: Users are stored in a Firebase Realtime Database; requires
|
|
|
|
* `firebaseKey` and `firebaseRtdbUrl` to be set.
|
|
|
|
**/
|
|
|
|
gatekeeperStore: "memory" | "firebase_rtdb";
|
|
|
|
/** URL of the Firebase Realtime Database if using the Firebase RTDB store. */
|
|
|
|
firebaseRtdbUrl?: string;
|
|
|
|
/** Base64-encoded Firebase service account key if using the Firebase RTDB store. */
|
|
|
|
firebaseKey?: string;
|
2023-05-14 13:42:56 -06:00
|
|
|
/**
|
|
|
|
* Maximum number of IPs per user, after which their token is disabled.
|
|
|
|
* Users with the manually-assigned `special` role are exempt from this limit.
|
|
|
|
* By default, this is 0, meaning that users are not IP-limited.
|
|
|
|
*/
|
2023-05-14 13:30:32 -06:00
|
|
|
maxIpsPerUser: number;
|
2023-04-08 17:32:49 -06:00
|
|
|
/** Per-IP limit for requests per minute to OpenAI's completions endpoint. */
|
2023-04-08 19:40:45 -06:00
|
|
|
modelRateLimit: number;
|
2023-04-08 17:32:49 -06:00
|
|
|
/** Max number of tokens to generate. Requests which specify a higher value will be rewritten to use this value. */
|
2023-04-08 17:57:35 -06:00
|
|
|
maxOutputTokens: number;
|
2023-04-08 20:49:06 -06:00
|
|
|
/** Whether requests containing disallowed characters should be rejected. */
|
|
|
|
rejectDisallowed?: boolean;
|
|
|
|
/** Message to return when rejecting requests. */
|
|
|
|
rejectMessage?: string;
|
2023-04-14 19:21:04 -06:00
|
|
|
/** Pino log level. */
|
2023-04-08 17:32:49 -06:00
|
|
|
logLevel?: "debug" | "info" | "warn" | "error";
|
2023-04-14 19:21:04 -06:00
|
|
|
/** Whether prompts and responses should be logged to persistent storage. */
|
2023-05-09 17:11:57 -06:00
|
|
|
promptLogging?: boolean;
|
2023-04-14 19:21:04 -06:00
|
|
|
/** Which prompt logging backend to use. */
|
2023-05-09 17:11:57 -06:00
|
|
|
promptLoggingBackend?: PromptLoggingBackend;
|
2023-04-14 19:21:04 -06:00
|
|
|
/** Base64-encoded Google Sheets API key. */
|
|
|
|
googleSheetsKey?: string;
|
|
|
|
/** Google Sheets spreadsheet ID. */
|
|
|
|
googleSheetsSpreadsheetId?: string;
|
2023-04-10 01:27:45 -06:00
|
|
|
/** Whether to periodically check keys for usage and validity. */
|
|
|
|
checkKeys?: boolean;
|
2023-05-09 17:11:57 -06:00
|
|
|
/**
|
|
|
|
* How to display quota information on the info page.
|
2023-05-12 18:58:15 -06:00
|
|
|
*
|
2023-05-13 22:26:08 -06:00
|
|
|
* `none`: Hide quota information
|
2023-05-12 18:58:15 -06:00
|
|
|
*
|
2023-05-13 22:26:08 -06:00
|
|
|
* `partial`: Display quota information only as a percentage
|
2023-05-12 18:58:15 -06:00
|
|
|
*
|
2023-05-13 22:26:08 -06:00
|
|
|
* `full`: Display quota information as usage against total capacity
|
2023-05-09 17:11:57 -06:00
|
|
|
*/
|
2023-05-11 19:35:28 -06:00
|
|
|
quotaDisplayMode: "none" | "partial" | "full";
|
2023-05-09 17:11:57 -06:00
|
|
|
/**
|
|
|
|
* Which request queueing strategy to use when keys are over their rate limit.
|
2023-05-12 18:58:15 -06:00
|
|
|
*
|
2023-05-13 22:26:08 -06:00
|
|
|
* `fair`: Requests are serviced in the order they were received (default)
|
2023-05-12 18:58:15 -06:00
|
|
|
*
|
2023-05-13 22:26:08 -06:00
|
|
|
* `random`: Requests are serviced randomly
|
2023-05-12 18:58:15 -06:00
|
|
|
*
|
2023-05-13 22:26:08 -06:00
|
|
|
* `none`: Requests are not queued and users have to retry manually
|
2023-05-09 17:11:57 -06:00
|
|
|
*/
|
|
|
|
queueMode: DequeueMode;
|
2023-04-08 17:32:49 -06:00
|
|
|
};
|
|
|
|
|
2023-04-10 04:39:13 -06:00
|
|
|
// To change configs, create a file called .env in the root directory.
|
|
|
|
// See .env.example for an example.
|
2023-04-08 17:32:49 -06:00
|
|
|
export const config: Config = {
|
|
|
|
port: getEnvWithDefault("PORT", 7860),
|
|
|
|
openaiKey: getEnvWithDefault("OPENAI_KEY", ""),
|
|
|
|
proxyKey: getEnvWithDefault("PROXY_KEY", ""),
|
2023-05-12 18:58:15 -06:00
|
|
|
adminKey: getEnvWithDefault("ADMIN_KEY", ""),
|
|
|
|
gatekeeper: getEnvWithDefault("GATEKEEPER", "none"),
|
2023-05-13 22:26:08 -06:00
|
|
|
gatekeeperStore: getEnvWithDefault("GATEKEEPER_STORE", "memory"),
|
2023-05-14 13:42:56 -06:00
|
|
|
maxIpsPerUser: getEnvWithDefault("MAX_IPS_PER_USER", 0),
|
2023-05-13 22:26:08 -06:00
|
|
|
firebaseRtdbUrl: getEnvWithDefault("FIREBASE_RTDB_URL", undefined),
|
|
|
|
firebaseKey: getEnvWithDefault("FIREBASE_KEY", undefined),
|
2023-04-10 04:39:13 -06:00
|
|
|
modelRateLimit: getEnvWithDefault("MODEL_RATE_LIMIT", 4),
|
|
|
|
maxOutputTokens: getEnvWithDefault("MAX_OUTPUT_TOKENS", 300),
|
2023-04-08 20:49:06 -06:00
|
|
|
rejectDisallowed: getEnvWithDefault("REJECT_DISALLOWED", false),
|
|
|
|
rejectMessage: getEnvWithDefault(
|
|
|
|
"REJECT_MESSAGE",
|
|
|
|
"This content violates /aicg/'s acceptable use policy."
|
|
|
|
),
|
2023-04-08 17:32:49 -06:00
|
|
|
logLevel: getEnvWithDefault("LOG_LEVEL", "info"),
|
2023-04-10 04:39:13 -06:00
|
|
|
checkKeys: getEnvWithDefault("CHECK_KEYS", !isDev),
|
2023-05-11 19:35:28 -06:00
|
|
|
quotaDisplayMode: getEnvWithDefault("QUOTA_DISPLAY_MODE", "partial"),
|
2023-04-14 19:21:04 -06:00
|
|
|
promptLogging: getEnvWithDefault("PROMPT_LOGGING", false),
|
|
|
|
promptLoggingBackend: getEnvWithDefault("PROMPT_LOGGING_BACKEND", undefined),
|
|
|
|
googleSheetsKey: getEnvWithDefault("GOOGLE_SHEETS_KEY", undefined),
|
|
|
|
googleSheetsSpreadsheetId: getEnvWithDefault(
|
|
|
|
"GOOGLE_SHEETS_SPREADSHEET_ID",
|
|
|
|
undefined
|
|
|
|
),
|
2023-05-09 17:11:57 -06:00
|
|
|
queueMode: getEnvWithDefault("QUEUE_MODE", "fair"),
|
2023-04-08 17:32:49 -06:00
|
|
|
} as const;
|
|
|
|
|
2023-05-12 18:58:15 -06:00
|
|
|
/** Prevents the server from starting if config state is invalid. */
|
2023-05-13 22:26:08 -06:00
|
|
|
export async function assertConfigIsValid() {
|
2023-05-12 18:58:15 -06:00
|
|
|
// Ensure gatekeeper mode is valid.
|
|
|
|
if (!["none", "proxy_key", "user_token"].includes(config.gatekeeper)) {
|
|
|
|
throw new Error(
|
|
|
|
`Invalid gatekeeper mode: ${config.gatekeeper}. Must be one of: none, proxy_key, user_token.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't allow `user_token` mode without `ADMIN_KEY`.
|
|
|
|
if (config.gatekeeper === "user_token" && !config.adminKey) {
|
|
|
|
throw new Error(
|
|
|
|
"`user_token` gatekeeper mode requires an `ADMIN_KEY` to be set."
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't allow `proxy_key` mode without `PROXY_KEY`.
|
|
|
|
if (config.gatekeeper === "proxy_key" && !config.proxyKey) {
|
|
|
|
throw new Error(
|
|
|
|
"`proxy_key` gatekeeper mode requires a `PROXY_KEY` to be set."
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't allow `PROXY_KEY` to be set for other modes.
|
|
|
|
if (config.gatekeeper !== "proxy_key" && config.proxyKey) {
|
|
|
|
throw new Error(
|
|
|
|
"`PROXY_KEY` is set, but gatekeeper mode is not `proxy_key`. Make sure to set `GATEKEEPER=proxy_key`."
|
|
|
|
);
|
|
|
|
}
|
2023-05-13 22:26:08 -06:00
|
|
|
|
|
|
|
// Require appropriate firebase config if using firebase store.
|
|
|
|
if (
|
|
|
|
config.gatekeeperStore === "firebase_rtdb" &&
|
|
|
|
(!config.firebaseKey || !config.firebaseRtdbUrl)
|
|
|
|
) {
|
|
|
|
throw new Error(
|
|
|
|
"Firebase RTDB store requires `FIREBASE_KEY` and `FIREBASE_RTDB_URL` to be set."
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-14 14:45:30 -06:00
|
|
|
// Ensure forks which add new secret-like config keys don't unwittingly expose
|
|
|
|
// them to users.
|
|
|
|
for (const key of getKeys(config)) {
|
|
|
|
const maybeSensitive = ["key", "credentials", "secret", "password"].some(
|
|
|
|
(sensitive) => key.toLowerCase().includes(sensitive)
|
|
|
|
);
|
|
|
|
const secured = new Set([...SENSITIVE_KEYS, ...OMITTED_KEYS]);
|
|
|
|
if (maybeSensitive && !secured.has(key))
|
|
|
|
throw new Error(
|
|
|
|
`Config key "${key}" may be sensitive but is exposed. Add it to SENSITIVE_KEYS or OMITTED_KEYS.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-13 22:26:08 -06:00
|
|
|
await maybeInitializeFirebase();
|
2023-05-12 18:58:15 -06:00
|
|
|
}
|
|
|
|
|
2023-05-13 22:26:08 -06:00
|
|
|
/**
|
2023-05-14 14:45:30 -06:00
|
|
|
* Config keys that are masked on the info page, but not hidden as their
|
|
|
|
* presence may be relevant to the user due to privacy implications.
|
2023-05-13 22:26:08 -06:00
|
|
|
*/
|
2023-05-14 14:45:30 -06:00
|
|
|
export const SENSITIVE_KEYS: (keyof Config)[] = ["googleSheetsSpreadsheetId"];
|
2023-05-12 18:58:15 -06:00
|
|
|
|
2023-05-14 14:45:30 -06:00
|
|
|
/**
|
|
|
|
* Config keys that are not displayed on the info page at all, generally because
|
|
|
|
* they are not relevant to the user or can be inferred from other config.
|
|
|
|
*/
|
2023-05-12 18:58:15 -06:00
|
|
|
export const OMITTED_KEYS: (keyof Config)[] = [
|
|
|
|
"port",
|
|
|
|
"logLevel",
|
|
|
|
"openaiKey",
|
|
|
|
"proxyKey",
|
|
|
|
"adminKey",
|
2023-05-14 14:45:30 -06:00
|
|
|
"checkKeys",
|
|
|
|
"quotaDisplayMode",
|
|
|
|
"googleSheetsKey",
|
|
|
|
"firebaseKey",
|
|
|
|
"firebaseRtdbUrl",
|
|
|
|
"gatekeeperStore",
|
|
|
|
"maxIpsPerUser",
|
2023-05-12 18:58:15 -06:00
|
|
|
];
|
|
|
|
|
2023-04-08 17:32:49 -06:00
|
|
|
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
|
2023-05-14 14:45:30 -06:00
|
|
|
|
2023-04-08 17:32:49 -06:00
|
|
|
export function listConfig(): Record<string, string> {
|
|
|
|
const result: Record<string, string> = {};
|
|
|
|
for (const key of getKeys(config)) {
|
|
|
|
const value = config[key]?.toString() || "";
|
2023-05-12 18:58:15 -06:00
|
|
|
|
2023-05-14 14:45:30 -06:00
|
|
|
const shouldOmit =
|
|
|
|
OMITTED_KEYS.includes(key) || value === "" || value === "undefined";
|
|
|
|
const shouldMask = SENSITIVE_KEYS.includes(key);
|
|
|
|
|
|
|
|
if (shouldOmit) {
|
2023-04-15 01:44:52 -06:00
|
|
|
continue;
|
|
|
|
}
|
2023-05-12 18:58:15 -06:00
|
|
|
|
2023-05-14 14:45:30 -06:00
|
|
|
if (value && shouldMask) {
|
2023-04-08 17:32:49 -06:00
|
|
|
result[key] = "********";
|
|
|
|
} else {
|
|
|
|
result[key] = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getEnvWithDefault<T>(name: string, defaultValue: T): T {
|
|
|
|
const value = process.env[name];
|
|
|
|
if (value === undefined) {
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
if (name === "OPENAI_KEY") {
|
|
|
|
return value as unknown as T;
|
|
|
|
}
|
|
|
|
return JSON.parse(value) as T;
|
|
|
|
} catch (err) {
|
|
|
|
return value as unknown as T;
|
|
|
|
}
|
|
|
|
}
|
2023-05-13 22:26:08 -06:00
|
|
|
|
|
|
|
let firebaseApp: firebase.app.App | undefined;
|
|
|
|
|
|
|
|
async function maybeInitializeFirebase() {
|
|
|
|
if (!config.gatekeeperStore.startsWith("firebase")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const firebase = await import("firebase-admin");
|
|
|
|
const firebaseKey = Buffer.from(config.firebaseKey!, "base64").toString();
|
|
|
|
const app = firebase.initializeApp({
|
|
|
|
credential: firebase.credential.cert(JSON.parse(firebaseKey)),
|
|
|
|
databaseURL: config.firebaseRtdbUrl,
|
|
|
|
});
|
|
|
|
|
|
|
|
await app.database().ref("connection-test").set(Date.now());
|
|
|
|
|
|
|
|
firebaseApp = app;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getFirebaseApp(): firebase.app.App {
|
|
|
|
if (!firebaseApp) {
|
|
|
|
throw new Error("Firebase app not initialized.");
|
|
|
|
}
|
|
|
|
return firebaseApp;
|
|
|
|
}
|